Issues While Trying To Recreate SwiftUI-based App From WWDC Session

Viewed 6052

I am trying to recreate the SwiftUI project demonstrated in Session 204 but am running into some peculiar issues.

I wrote this from watching the session found here: https://developer.apple.com/videos/play/wwdc2019/204

Here is my code-

ContentView.swift:

import SwiftUI
struct ContentView : View {
    @ObjectBinding var store = RoomStore()

    var body: some View {
        NavigationView {
            List {
                Section {
                    Button(action: addRoom) {
                        Text("Add Room")
                    }
                }

                Section {

                    ForEach(store.rooms) { room in //Error: Cannot convert value of type '(Room) -> RoomCell' to expected argument type '(_) -> _'
                        RoomCell(room: room)
                    }
                    .onDelete(perform: delete)
                    .onMove(perform: move)
                }
            }
            .navigationBarTitle(Text("Rooms") )
            .NavigationBarItems(trailing: EditButton())
            .listStyle(.grouped)
        }
    }

    func addRoom() {
        store.rooms.append(Room(name: "Hall 2", capacity: 2000))
    }
    func delete(at offsets: IndexSet) {
        store.rooms.remove(atOffsets: offsets) //Error: Incorrect argument label in call (have 'atOffsets:', expected 'at:')
    }
    func move(from source: IndexSet, to destination: Int) {
        store.rooms.move(fromOffsets: source, toOffset: destination) //Error: Value of type '[Room]' has no member 'move'; did you mean 'remove'?
    }
}

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        Group {
            ContentView(store: RoomStore(rooms: testData))
            ContentView(store: RoomStore(rooms: testData))
            .environment(\.sizeCategory, .extraExtraExtraLarge)
            ContentView(store: RoomStore(rooms: testData))
            .environment(\.colorScheme, .dark)
            ContentView(store: RoomStore(rooms: testData))
            .environment(\.layoutDirection, .rightToLeft)
            .environment(\.locale, Locale(identifier: "ar"))
        }
    }
}
#endif
struct RoomCell : View {
    let room: Room
    var body: some View {
        return NavigationButton(destination: RoomDetail(room: room) )
        {
            Image(room.thumbnailName)
            .cornerRadius(8)
            VStack(alignment: .leading) {
                Text (room.name)
                Text ("\(room.capacity) peopje")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
        }
    }
}

Room.swift:

import SwiftUI

struct Room {
    var id = UUID()
    var name: String
    var capacity: Int
    var hasVideo: Bool = false
    var imageName: String { return name }
    var thumbnailName: String { return name + "Thumb" }

}

#if DEBUG

let testData = [
    Room(name: "Observation Deck", capacity: 6, hasVideo: true),
    Room(name: "Executive Suite", capacity: 8, hasVideo: false),
    Room(name: "Charter Jet", capacity: 16, hasVideo: true),
    Room(name: "Dungeon", capacity: 10, hasVideo: true),
    Room(name: "Panorama", capacity: 12, hasVideo: false),
    Room(name: "Oceanfront", capacity: 8, hasVideo: false),
    Room(name: "Rainbow Room", capacity: 10, hasVideo: true),
    Room(name: "Pastoral", capacity: 7, hasVideo: false),
    Room(name: "Elephant Room", capacity: 1, hasVideo: false),

]

#endif

RoomDetail.swift:

import SwiftUI
struct RoomDetail : View {
    let room: Room
    @State private var zoomed = false
    var body: some View { //Error: Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type
        ZStack(alignment: .topLeading) {
            Image(room.imageName )
                .resizable()
                .aspectRatio(contentMode: zoomed ? .fill : .fit)
                .navigationBarTitle(Text(room.name), displayMode:
                    .inline)
                .tapAction { withAnimation(.basic(duration: 2)) {
                    self.zoomed.toggle() } }
            }
            .frame(minWidth: 0, maxWidth: .infinity, minHeight:
                0, maxHeight: .infinity)
            if room.hasVideo && !zoomed {
                Image(systemName: "video. fill")
                    .font(.title)
                    .padding(.all)
                    .transition(.move(edge: .leading) )
            }
        }
}

#if DEBUG
struct RoomDetail_Previews : PreviewProvider {
    static var previews: some View {
        Group {
            NavigationView { RoomDetail(room: testData[0]) }
            NavigationView { RoomDetail(room: testData[1]) }
        }
}
}
#endif

RoomStore.swift:

import SwiftUI
import Combine

class RoomStore : BindableObject {
    var rooms: [Room] {
        didSet { didChange.send(Void()) } //Solved
    }
    init(rooms: [Room] = []) {
        self.rooms = rooms
    }
    var didChange = PassthroughSubject<Void, Never>()
}

Error messages included in context, as comments, in above code.

7 Answers

For this error: //Error: Cannot convert value of type '(Room) -> RoomCell' to expected argument type '(_) -> _' Implement Identifiable protocol to your list model like this,

struct Room: Identifiable

For this one: //Error: Incorrect argument label in call (have 'atOffsets:', expected 'at:') I think it is not your problem :) But you can use something like this,

guard let index = Array(offset).first else { return }
store.rooms.remove(at: index)

And for this one: //Error: Value of type '[Room]' has no member 'move'; did you mean 'remove'? As before, you can use that piece of code for moving

guard let sourceIndex = Array(source).first else { return }
store.rooms.insert(roomStore.rooms.remove(at: sourceIndex), at: destination)

You can check completed source code, https://github.com/ilyadaberdil/iOS-Samples/tree/master/SwiftUI-Sample

Did you try passing Void()?

class RoomStore : BindableObject {
    var rooms: [Room] {
        didSet { didChange.send(Void()) } 
    }
    init(rooms: [Room] = []) {
        self.rooms = rooms
    }
    var didChange = PassthroughSubject<Void, Never>()
}

In Xcode 11 beta 4

The BindableObject protocol’s requirement is now willChange instead of didChange, and should now be sent before the object changes rather than after it changes. This change allows for improved coalescing of change notifications.

class RoomStore: BindableObject {

    var rooms: [Room] {
        didSet { willChange.send() }
    }

    init(rooms: [Room] = []) {
        self.rooms = rooms
    }

    var willChange = PassthroughSubject<Void, Never>()
}

For deleting and moving, use below code.

func delete(at offsets: IndexSet) {
    store.rooms.remove(atOffsets: offsets)
}

func move(from source: IndexSet, to destination: Int) {
    store.rooms.move(fromOffsets: source, toOffset: destination)
}

Rooms has to be @Published

class RoomStore: ObservableObject {
    @Published var rooms: [Room] {
        didSet {
            willChange.send()
        }
    }

    init(rooms: [Room] = []) {
        self.rooms = rooms
    }

   var willChange = PassthroughSubject<Void, Never>()
}

To use Room in your List, you have to implement the Identifiable protocol. I forgot it too and the SwiftUI error message:

Cannot convert value of type '(Room) -> RoomCell' to expected argument type '(_) -> _'

is not helpful.

import SwiftUI

struct Room: Identifiable {
    let id = UUID() 
    ...
}

For deleting and moving you can use something like this:

func delete(at offsets: IndexSet) {
    offsets.sorted { $0 > $1 }.forEach { store.rooms.remove(at: $0) }
}

func move(from source: IndexSet, to destination: Int) {
    source.sorted { $0 > $1 }.forEach { store.rooms.insert(store.rooms.remove(at: $0), at: destination) }
}

According to https://developer.apple.com/tutorials/swiftui/handling-user-input section 4, step 2, the PassthroughSubject should take the type of the class you're binding, not Void. So it would seem to be more correct (and works).

var didChange = PassthroughSubject<RoomStore, Never>()

As for the delete and move functions, since Swift 5.1 arrays have neither a move nor a remove(atOffsets:) method I can only assume they forgot to remove some custom extensions to Array. I could find no mention of those functions on Swift Evolution either.

Here's hoping they just forgot to tell us they're coming in a later release.

:) Teo

Related