UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Code review request, for a noob to Swift/SwiftUI

Forums > SwiftUI

Hey there, one and all!

Pretty new to the world of Swift and development on iOS, still waiting to become an Apple developer. But in the meantime I figured I would "get my hands dirty" and fiddle about with Xcode and SwiftUI for a bit. Do note that I am "stuck" on Xcode 12.4 (Swift 5.3 if I'm not mistaken) and macOS 10.15 (still have an old MacBook Pro, looking to upgrade sometime next year). Not new to programming, having started with Basic on MSX, C on Amiga, Asm on PC and professionally as front- and backend developer for web; Javascript, PHP. Also experience with Python, Ruby and Rust.

The code in question (posted below) will be sending a "heartbeat" to an external server (in the final code) every 2 seconds, and will be receiving data on port 9876 at a rate of up to 60 Hz. As it stands, not being a registered Apple developer I am stuck on just listening to the UDP port and can't send the "heartbeat", but I don't foresee any real issue with that part.

I have verified that this works at least in the Xcode simulators, but I figured maybe someone could take a look and see if anything sticks out as a "heck no, don't do that!" 😂

//
//  ContentView.swift
//

import SwiftUI

struct ContentView: View {
    @ObservedObject var pollUdp: PollUDP
    var body: some View {
        VStack {
            Button(action: {self.pollUdp.testServer()}) {
                Text("Start Server")
            }
            RawIntegerView(value: $pollUdp.packetNumber)
            Percent1DigitView(value: $pollUdp.waterLevel)
            Percent2DigitView(value: $pollUdp.waterTemp)
        }
    }
}

struct RawIntegerView : View {
    @Binding var value : UInt32
    var body: some View {
        return Text(String(value))
    }
}
struct Percent1DigitView : View {
    @Binding var value : Float32
    var body: some View {
        return Text(String(format: "%.1f%%", value))
    }
}
struct Percent2DigitView : View {
    @Binding var value : Float32
    var body: some View {
        return Text(String(format: "%.2f%%", value))
    }
}

class PollUDP: ObservableObject {
    @Published var packetNumber: UInt32 = 0
    @Published var waterLevel: Float32 = 0.0
    @Published var waterTemp: Float32 = 0.0

    func testServer() {

        // in live version will update up to 60 times a second
        DispatchQueue.global().async {
            let server = UDPServer(address: "127.0.0.1", port: 9876)
            while true {
                let (data, _, _) = server.recv(1024)
                //...
                DispatchQueue.main.async {
                    self.packetNumber = data.packetNumber
                }
            }
        }

        // polling every 5 seconds
        DispatchQueue.global().async {
            while true {
                sleep(5)
                DispatchQueue.main.async {
                    self.waterLevel = Float32.random(in: 0.0...100.0)
                }
            }
        }

        // polling every 2 seconds
        DispatchQueue.global().async {
            while true {
                sleep(2)
                DispatchQueue.main.async {
                    self.waterTemp = Float32.random(in: 0.0...20.0)
                }
            }
        }

    }
}

1      

One line 3 instead of @ObservedObject var pollUdp: PollUDP use @StateObject var pollUdp = PollUDP() ,

SwiftUI’s @StateObject property wrapper is a specialized form of @ObservedObject, having all the same functionality with one important addition: it should be used to create observed objects, rather than just store one that was passed in externally.

When you add a property to a view using @StateObject, SwiftUI considers that view to be the owner of the observable object. All other views where you pass that object should use @ObservedObject.

So, to be clear: you should create your observable object somewhere using @StateObject, and in all subsequent places where you pass that object you should use @ObservedObject.

Sample

// An example class to work with
class Player: ObservableObject {
    @Published var name = "Taylor"
    @Published var age = 26
}

// A view that creates and owns the Player object.
struct ContentView: View {
    @StateObject var player = Player()

    var body: some View {
        NavigationView {
            NavigationLink(destination: PlayerNameView(player: player)) {
                Text("Show detail view")
            }
        }
    }
}

// A view that monitors the Player object for changes, but
// doesn't own it.
struct PlayerNameView: View {
    @ObservedObject var player: Player

    var body: some View {
        Text("Hello, \(player.name)!")
    }
}

Disclaimer - i am also a noob 😁

2      

Ahh, I see, I have to read up on that, but I get the distinction.

As it stands I instantiate it in the @main in the TestApp.swift, like this:

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView(pollUdp: PollUDP())
        }
    }
}

But I surmise that having the ContentView own it is better.

I'm mostly amazed I managed to get it going as far as this in a week. That includes compiling a Rust salsa20 decoding (a special doohickey) framework to handle the data received by the UDP connection. I was unable to get a pre-existing salsa20 implementation to work properly with my limited understanding of the intricacies of Swift (possibly related to me being on an older version of Xcode and Swift), so I am happy with the progress at least :D

1      

Something that you may or may not already know. The return keyword is actually unnecessary in these...

struct RawIntegerView : View {
    @Binding var value : UInt32
    var body: some View {
        return Text(String(value))
    }
}
struct Percent1DigitView : View {
    @Binding var value : Float32
    var body: some View {
        return Text(String(format: "%.1f%%", value))
    }
}
struct Percent2DigitView : View {
    @Binding var value : Float32
    var body: some View {
        return Text(String(format: "%.2f%%", value))
    }
}

In Swift, if you have a closure that is supposed to return a value, and you only have one line of code in that closure, it will automatically assume that the value expressed in that one line of code is the value that should be returned.

What you have written isn't really wrong. But, just thought I'd point it out in case you didn't know. So you could write it like this...

struct RawIntegerView : View {
    @Binding var value : UInt32
    var body: some View {
        Text(String(value))
    }
}
struct Percent1DigitView : View {
    @Binding var value : Float32
    var body: some View {
        Text(String(format: "%.1f%%", value))
    }
}
struct Percent2DigitView : View {
    @Binding var value : Float32
    var body: some View {
        Text(String(format: "%.2f%%", value))
    }
}

2      

Indeed @Fly0strich, now that you mention it, I kind of knew that already. But thanks for pointing it out, and as you say; not wrong, but I may as well do it the conventional way from the get-go. Harder to get rid of bad coding habits later on.

Having too many various programming languages stored away, it gets you sometimes :D

My mind is too single-threaded in these multi-threaded times!

1      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.