Categories
Uncategorized

navigationBarTitle Display Inline when sharing code between iOS and watchOS

I came across an issue when building a SwiftUI cross platform app recently. I did not want a large navigationBarTitle on iOS, I wanted it in the toolbar, but if I set displayMode to inline the watchOS version will not compile as it is not supported there. Basically the following code won’t work on watchOS.

NavigationView {
.navigationBarTitle("Title", displayMode: .inline)
}

I did not want to have to have a lot of platform specific code so a workaround I found is to declare navigationBarTitle without the displayMode set to inline and then add iOS #if code to add to the toolbar, then extend the View object and navigationBarTitle on iOS which will hide navigationBarTitle and hide the padding it creates at the top. Like below.

#if os(iOS)
 extension View {
     func navigationBarTitle(_ title: String) -> some View {
         self.navigationBarTitleDisplayMode(.inline)
     }
 }
 #endif 

// Then you can just do this anywhere for iOS and watchOS.
.navigationBarTitle("Title")

//and then in your view, to show a title in the toolbar for iOS  

#if os(iOS)
ToolbarItem(placement: .principal) {
    Text("Title")
}
#endif 
Categories
coding

A stopwatch timer class for SwiftUI

I spent sometime recently for Beep Test Watch creating a timer class in swift that publishes a nicely formatted string with the timer as “00:00” being mm:ss. Figured I would share it here if anyone stumbled upon it and found it useful.

import Combine
import Foundation

class TimerManager: NSObject, ObservableObject {
    @Published var elapsedSeconds: Int16 = 0
    @Published var elapsedSecondsFormatted: String = "00:00"

    var start: Date = Date()
    // Holds timer
    var cancellable: Cancellable?
    var accumulatedTime: Int16 = 0

    func setUpTimer() {
        start = Date()
        cancellable = Timer.publish(every: 0.1, on: .main, in: .default)
            .autoconnect()
            .sink { [weak self] _ in
                guard let self = self else { return }
                self.elapsedSeconds = self.incrementElapsedTime()
                self.elapsedSecondsFormatted = self.getFormatedTime(seconds: self.elapsedSeconds)
            }
    }

    // Calculate the elapsed time.
    func incrementElapsedTime() -> Int16 {
        let runningTime: Int16 = Int16(-1 * (self.start.timeIntervalSinceNow))
        return self.accumulatedTime + runningTime
    }

    func endTimer() {
        // Cancel timer and set elapsedSeconds Publisher to 0
        cancellable?.cancel()
        elapsedSeconds = 0
    }

    // Creates spotwatch formatted version of time
    func getFormatedTime(seconds: Int16) -> String {
        return self.elapsedTimeString(elapsed: self.secondsToHoursMinutesSeconds(seconds: seconds))
    }

    // Convert the seconds into seconds, minutes, hours.
    func secondsToHoursMinutesSeconds (seconds: Int16) -> (Int16, Int16, Int16) {
        return (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60)
    }

    // Convert the seconds, minutes, hours into a string.
    func elapsedTimeString(elapsed: (h: Int16, m: Int16, s: Int16)) -> String {
        return String(format: "%02d:%02d", elapsed.m, elapsed.s)
    }
}

In order to use it add an environmental object to your ContentView() in your main app file.

import SwiftUI
var timerManager = TimerManager()

@main
struct Your_App: App {
    var body: some Scene {
        WindowGroup {
                ContentView()
                    .environmentObject(timerManager)
        }
    }
}


struct ContentView: View {
    @EnvironmentObject var timerManager: TimerManage
    var body: some View { ... }
}

I’ve made a playground file as well that works standalone that shows you how to use it below.

import Combine
import Foundation
import SwiftUI
import PlaygroundSupport


// A timer class that publishes elapsedSeconds since setUpTimer was called as in integer as well as elapsedSecondsFormatted a formatted version that is "mm:ss", note the hour is dropped. Must call endTimer when timer is no longer needed to cancel timer.
class TimerManager: NSObject, ObservableObject {
    @Published var elapsedSeconds: Int16 = 0
    @Published var elapsedSecondsFormatted: String = "00:00"

    var start: Date = Date()
    // Holds timer
    var cancellable: Cancellable?
    var accumulatedTime: Int16 = 0

    func setUpTimer() {
        start = Date()
        cancellable = Timer.publish(every: 0.1, on: .main, in: .default)
            .autoconnect()
            .sink { [weak self] _ in
                guard let self = self else { return }
                self.elapsedSeconds = self.incrementElapsedTime()
                self.elapsedSecondsFormatted = self.getFormatedTime(seconds: self.elapsedSeconds)
            }
    }

    // Calculate the elapsed time.
    func incrementElapsedTime() -> Int16 {
        let runningTime: Int16 = Int16(-1 * (self.start.timeIntervalSinceNow))
        return self.accumulatedTime + runningTime
    }

    func endTimer() {
        // Cancel timer and set elapsedSeconds Publisher to 0
        cancellable?.cancel()
        elapsedSeconds = 0
    }

    // Creates spotwatch formatted version of time
    func getFormatedTime(seconds: Int16) -> String {
        return self.elapsedTimeString(elapsed: self.secondsToHoursMinutesSeconds(seconds: seconds))
    }

    // Convert the seconds into seconds, minutes, hours.
    func secondsToHoursMinutesSeconds (seconds: Int16) -> (Int16, Int16, Int16) {
        return (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60)
    }

    // Convert the seconds, minutes, hours into a string.
    func elapsedTimeString(elapsed: (h: Int16, m: Int16, s: Int16)) -> String {
        return String(format: "%02d:%02d", elapsed.m, elapsed.s)
    }
}

// Maybe publish the formatted version as well to remove redundant code.

var timerManager = TimerManager()

struct ContentView: View {

    @EnvironmentObject var timerManager: TimerManager
    var body: some View {
        Text("TIME")
            .font(.system(size: 12))
            .foregroundColor(.red)
        Text("\(timerManager.elapsedSecondsFormatted)")
            .padding(.top, -8.0)
            .font(Font.system(size: 16, weight: .semibold, design: .default).monospacedDigit())
        Button("start", action: startTimer)
        Button("end", action: endTimer)
    }
    func startTimer() {
        timerManager.setUpTimer()
    }
    func endTimer() {
        timerManager.endTimer()
    }

}

PlaygroundPage.current.setLiveView(
    ContentView()
        .environmentObject(timerManager)
)

Categories
Uncategorized

How to get data from Woolworths Online

I get a lot of questions about how I built discountkit.com.au from people looking to build apps to use data from Woolworths Online. I don’t have any official API access to the Woolworths Online API, I basically just consume the endpoints that their public facing website does.

The Woolworths Online website is AngularJS based and has public facing json data that you can find the endpoints to using Chrome DevTools.

Basically head to a product or page you want to get data from such as Almond Kernals. Open DevTools, go to the ‘Network’ tab and click ‘XHR’ as shown in the image below. Then refresh the page.

If you then hover over the items in the Name column, you will see a URL for the endpoint of that data. Right click > Copy > Copy Link Address.

In this case right clicking ‘176111’ the product id gives me the following url: https://www.woolworths.com.au/api/v3/ui/schemaorg/product/176411

If you head to that URL now you can see JSON data which you can then use to easily get what data you need.

Hopefully this helps!