AtelierClockwork

Showing the header

Now that there’s a working game board, we need a header that shows how many mines are left, how much time has elapsed, and the win / loss state. It’s a simple view, but there are a few interesting tricks involved.

The first view of interest is the timer view:

private struct TimerView: View {
    @ObservedObject var gameService: GameService
    @State private var elapsedTime: TimeInterval = 0

    var body: some View {
        Text("\(elapsedTime, format: .number.precision(.integerAndFractionLength(integer: 3, fraction: 0)))")
            .monospacedDigit()
            .onReceive(Timer.publish(every: 0.2, on: .main, in: .default).autoconnect()) { _ in
                elapsedTime = gameService.elapsedTime
            }
    }
}

The game service can return an elapsed time, but we need to use a Timer to force the view to update. The game service could do that internally, but that would invalidate all views that observe the service, so it’s less disruptive to the rest of the app to put the timer in this view.

Next we have the time label, mines label, and action button. One minor detail of note is the modern format syntax available in iOS 15.

private extension Header {
    private var timeLabel: some View {
        HStack(alignment: .firstTextBaseline, spacing: 0) {
            Text("Time:")
                .foregroundColor(.secondary)
                .font(.callout)
            TimerView(gameService: gameService)
        }
    }

    private var minesLabel: some View {
        HStack(alignment: .firstTextBaseline, spacing: 0) {
            Text("Mines:")
                .foregroundColor(.secondary)
                .font(.callout)
            Text("\(gameService.remainingMines, format: .number.precision(.integerLength(3)))")
                .monospacedDigit()
        }
    }

    private var actionButton: some View {
        Button {
            gameService.resetGame()
        } label: {
            Image(systemName: gameService.state.image)
        }
        .buttonStyle(.borderedProminent)
    }
}

private extension Game.State {
    var image: String {
        switch self {
        case .active:
            return "face.smiling.fill"
        case .win:
            return "flag.checkered"
        case .lose:
            return "xmark.seal.fill"
        }
    }
}

Finally, the whole thing is wrapped up in a view modifier, and is packaged to use the toolbar on iOS, but to inject the views as a header on macOS because I couldn’t quite get the right layout of content in the menu bar.

Now the game is play-able, so next post is going to be pulling in the ability to right click on the mac, and the project will be some level of done for now.