Weblog entries 2021

2021-12-16 Switch with multiple cases with associated values

Lots of people know about switch cases with associated values, for example as follows:

    enum Emoji {
        case lol(String)
        case ohnoes(String)
        case nothing
    }

Some code that uses the above enum:

    let testcases: [Emoji] = [
        .lol("🙂"),
        .ohnoes("😒"),
        .nothing
    ]
    for testcase in testcases {
        switch testcase {
        case .lol(let emoji):
            print("Happy emoji: " + emoji)
        case .ohnoes(let emoji):
            print("Sad emoji: " + emoji)
        case .nothing:
            print("No emoji")
        }
    }

If however, you have common code that must run for both happy and sad emoji, it's possible to combine them into one case. However inside that case, you can still test for one specific case. The example below shows how you can print common code for both happy and sad emoji, but still detect happy emoji:

    for testcase in testcases {
        switch testcase {
        case .lol(let emoji), .ohnoes(let emoji):
            if case .lol = testcase {
                print("Happy emoji detected: \(emoji)")
            }
            print("This is code that runs on any emoji")
        case .nothing:
            print("No emoji")
        }
    }

2021-12-02 SwiftUI NavigationLink Extraneous argument label isActive in call

Today, in Xcode 13.1, I got the following error:

    Extraneous argument label 'isActive:' in call

Accompanied by:

    Type '() -> EmptyView' cannot conform to 'StringProtocol'

This can be easily reproduced with the following code:

    struct ContentView: View {
        @State private var navigate = false
        var body: some View {
            NavigationLink(isActive: self.$navigate,
                           destination: Text("Hello, world!")) {
                EmptyView()
            }
        }
    }

Can you spot the problem above? I couldn't.

An alternative is when you try and specify the label parameter:

    struct ContentView: View {
        @State private var navigate = false
        var body: some View {
            NavigationLink(isActive: self.$navigate,
                           destination: Text("Hello, world!"),
                           label: {
                EmptyView()
            })
        }
    }

Then your errors become somewhat more clear:

    Generic parameter 'Destination' could not be inferred
    Cannot convert value of type 'Text' to expected argument type '() -> Destination'

Namely: the expected argument type to the destination parameter is a closure. As follows:

    destination: { Text("Hello, world!") }

2021-10-20 Generic parameter Destination could not be inferred

Xcode 13.0. If you're banging out SwiftUI code, and you got the following error message:

    Generic parameter 'Destination' could not be inferred

and it'll follow up with the following error as well (if you tried to navigate to a Text view):

    Cannot convert value of type 'Text' to expected argument type '() -> Destination'

then you probably tried to create a NavigationLink and let auto-complete add the following code:

    NavigationLink(tag: Hashable, selection: Binding<Hashable?>, destination: () -> _, label: () -> _)

The solution is to not use the auto-complete version of NavigationLink, but instead use a different sequence of parameters:

    NavigationLink(destination: () -> _, tag: Hashable, selection: Binding<Hashable?>, label: () -> _)

2021-10-19 Constrained extension must be declared on the unspecialized generic type

Did you get the following error message in Swift?

    Constrained extension must be declared on the unspecialized generic type 'Optional' with constraints specified by a 'where' clause

I got that one, and I tried to extend an optional string as follows:

    extension Optional<String> {
        func ifEmpty(_ replacementString: String) -> String? {
            (self ?? "").isEmpty ? replacementString : self
        }
    }

The correct syntax is:

    extension Optional where Wrapped == String {
        func ifEmpty(_ replacementString: String) -> String? {
            (self ?? "").isEmpty ? replacementString : self
        }
    }

It's still painful that it must return an optional. That can be avoided by a static function, which isn't as nice of course :)

2021-09-30 CGRect appendInterpolation

In the category of errors that you google but don't get results.

Suppose you have the following state variable in a SwiftUI view:

    @State private var textFieldFrame1 = CGRect()    

And you want to display that somewhere, as follows:

    Text("Frame of textfield 1 = \(textFieldFrame1)")

You'll get the following compiler error:

    No exact matches in call to instance method 'appendInterpolation'

The solution is as follows:

    Text("Frame of textfield 1 = " + String(describing: self.textFieldFrame1))

2021-03-22 Debugging view sizes

Here's a tip if you wish to show the sizes of views in your SwiftUI previews. Add the following struct to your project.

    struct ViewSizePreferenceReader: View {
        private struct ViewSizePreferenceKey: PreferenceKey {
            typealias Value = [CGSize]
            static var defaultValue: [CGSize] = []
            static func reduce(value: inout [CGSize], nextValue: () -> [CGSize]) {
                value.append(contentsOf: nextValue())
            }
        }
        @Binding var sizeString: String
        var body: some View {
            GeometryReader { (geometry: GeometryProxy) in
                Color.clear
                    .preference(key: ViewSizePreferenceKey.self, value: [geometry.size])
            }
            .onPreferenceChange(ViewSizePreferenceKey.self) { preferences in
                if let size = preferences.first {
                    self.sizeString = String(format: "%.1f x %.1f", size.width, size.height)
                }
            }
        }
    }

Then add a @State variable for the string, and use the above view as a background, as follows. The preview below will show how the Dynamic Type setting influences SF Symbol based images.

    struct ContentView: View {
        @State private var sizeString = "0x0"
        var body: some View {
            VStack {
                Image(systemName: "magnifyingglass")
                    .imageScale(.large)
                    .background(ViewSizePreferenceReader(sizeString: self.$sizeString))
                Text("Size of button: \(self.sizeString)")
            }
        }
    }
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            Group {
                ContentView()
                ContentView()
                    .environment(\.sizeCategory,
                                 .accessibilityLarge)
            }
            .previewLayout(.fixed(width: 414, height: 200))
        }
    }

2021-01-22 Error in SwiftUI Previews part deux

Today Xcode refused to show previews for one view, displaying "Cannot preview this file" on top of the previews canvas.

This time it's showing the error "SomeUnitTest.swift must belong to at least one target in the current scheme in order to use previews". I have the feeling that this error is incorrect and masks a problem elsewhere, because unittests most certainly should not belong to any other target than the test target.

I have been able to temporarily fix the problem as follows:

  • Quit Xcode and simulator
  • Delete the ~/Library/Developer/Xcode/DerivedData folder
  • Start Xcode and simulator
  • Remove all unittests from the project (don't move to trash, simply remove references)
  • Compile the project
  • Add the unittests back to the project