quigs.blog

howto

Have you noticed that all the examples on Apple's website, on Medium, and by prominent SwiftUI bloggers only use List views for the sidebar(s) and don't have a NavigationStack in the final detail view that they push additional content onto? There's a reason for that, and unfortunately it's not a pleasant one.

If you're looking to build a seemingly simple split-view app with a 1-depth sidebar and an n-depth detail view, using List in the sidebar view is a fine choice. But if you do not want the cookie-cutter look and feel List is not your friend. Unfortunately, NavigationSplitView really, really wants you to use List and if you don't use one then don't you dare try to programmatically push more content onto the detail view's navigation stack.

This split view demo from WWDC looks great, but what if your side-bar UI would look better if it were layed out as a grid?

At first glance, dropping the sidebar List view seems to work fine. I will be modifying Majid Jabrayilov's excellent split view code example to demonstrate the limitations (shortcomings? Bugs?) of SwiftUI 4's NavigationSplitView. First let's keep the List-backed sidebar and try to programmatically navigate two levels deep in the detail view:

struct ContentView: View {
    @State private var selectedItem: String?
    @State private var navPath = NavigationPath()
    
    @State private var folders = [
        "All": [
            "Item1",
            "Item2"
        ],
        "Favorites": [
            "Item2"
        ]
    ]
    
    var body: some View {
        NavigationSplitView {
            List(selection: $selectedItem) {
                ForEach(folders["All", default: []], id: \.self) { item in
                    NavigationLink(value: item) {
                        Text(verbatim: item)
                    }
                }
            }
            .navigationTitle("All")
        } detail: {
            NavigationStack(path: $navPath) {
                VStack {
                    if let selectedItem {
                        Button("Button \(selectedItem)") {
                            navPath.append(selectedItem)
                        }
                    } else {
                        Text("Choose an item from the content")
                    }
                }
                .navigationDestination(for: String.self) { text in
                    Text(verbatim: text)
                        .navigationTitle(text)
                }
            }
        }
    }
}

This works great, and if you select the “Button ItemN” button, you will see it programmatically pushes a view onto the detail view's stack.

A 2-level deep detail view on a NavigationSplitView running on an iPhone/compact horizontal size class.

Now let's get rid of that bog-standard List view in the sidebar and replace it with a horizontal grid:

struct ContentView: View {
    @State private var selectedItem: String?
    @State private var navPath = NavigationPath()
    
    @State private var folders = [
        "All": [
            "Item1",
            "Item2"
        ],
        "Favorites": [
            "Item2"
        ]
    ]
    
    let rows = [GridItem(.fixed(30))]
    
    var body: some View {
        NavigationSplitView {
            ScrollView {
                LazyHGrid(rows: rows) {
                    ForEach(folders["All", default: []], id: \.self) { item in
                        NavigationLink(value: item) {
                            Text(verbatim: item)
                        }
                    }
                }
            }
            .navigationTitle("All")
        } detail: {
            NavigationStack(path: $navPath) {
                VStack {
                    if let selectedItem {
                        Button("Button \(selectedItem)") {
                            navPath.append(selectedItem)
                        }
                    } else {
                        Text("Choose an item from the content")
                    }
                }
                .navigationDestination(for: String.self) { text in
                    Text(verbatim: text)
                        .navigationTitle(text)
                }
            }
        }
    }
}

It looks great (🙃). Let's test and then ship it!

A svelte grid layout in the side bar instead of a List . 🔥!

As soon as we select an item though, we get a runtime error:

A NavigationLink is presenting a value of type “String” but there is no matching navigationDestination declaration visible from the location of the link. The link cannot be activated.

Ugh. Let's fix that by providing the sidebar view with a .navigationDestination. We will move the detail view to it's own struct so it can be re-used.

struct ContentView: View {
    @State private var selectedItem: String?
    @State private var navPath = NavigationPath()
    
    @State private var folders = [
        "All": [
            "Item1",
            "Item2"
        ],
        "Favorites": [
            "Item2"
        ]
    ]
    
    let rows = [GridItem(.fixed(30))]
    
    var body: some View {
        NavigationSplitView {
            ScrollView {
                LazyHGrid(rows: rows) {
                    ForEach(folders["All", default: []], id: \.self) { item in
                        NavigationLink(value: item) {
                            Text(verbatim: item)
                        }
                    }
                }
            }
            .navigationDestination(for: String.self) { selection in
                DetailView(navPath: $navPath, selectedItem: selection)
            }
            .navigationTitle("All")
        } detail: {
            NavigationStack(path: $navPath) {
                DetailView(navPath: $navPath, selectedItem: selectedItem)
            }
        }
    }
}

struct DetailView: View {
    @Binding var navPath: NavigationPath
    let selectedItem: String?
    var body: some View {
        VStack {
            if let selectedItem {
                Button("Button \(selectedItem)") {
                    navPath.append(selectedItem)
                }
            } else {
                Text("Choose an item from the content")
            }
        }
        .navigationDestination(for: String.self) { text in
            Text(verbatim: text)
                .navigationTitle(text)
        }
    }
}

When we run this we get to the first level of the detail view, but when we select the “Button ItemN” programmatic navigation button... nothing happens. And this is where it all falls apart.

As of this writing, Jan 1, 2023, on Xcode 14.2 there is no way (that I've found) to programmatically navigate to the second level of the detail view. SwiftUI just doesn't do anything. There are no errors or warnings in the debug console. There is just silence and no navigation.

In fact, the only way to get the detail view to push a second level deep without a List-backed sidebar is to get rid of programmatic navigation on the detail view altogether and replace the button with a NavigationLink:

struct DetailView: View {
    @Binding var navPath: NavigationPath
    let selectedItem: String?
    var body: some View {
        VStack {
            if let selectedItem {
                NavigationLink(value: selectedItem) {
                    Text("NavigationLink \(selectedItem)")
                }
            } else {
                Text("Choose an item from the content")
            }
        }
        .navigationDestination(for: String.self) { text in
            Text(verbatim: text)
                .navigationTitle(text)
        }
    }
}

A navigation split view with a grid-based “sidebar” view and a NavigationLink in the first detail view.

Great! It works now—but only if your user selects the navigation link directly. As with many things in SwiftUI over the last 4 years, this feels like such simple edge case, and yet there's nothing that can be done about it. Our options are either:

  1. Host a UISplitViewController in our SwiftUI app and manually handle navigation.
  2. Don't programmatically navigate after the first detail view.
  3. Use a List for the sidebar view and deal with the visual constraints of a List.

Hopefully this is something the SwiftUI team at Apple can resolve soon. iOS 16 brought a huge quality-of-life improvement to navigation on SwiftUI. It would be nice to see this fixed before iOS 17, but until then if you require a unique-looking UI that navigates sanely, don't let your UIKit skills atrophy just yet. SwiftUI continues to be “close” but not quite “there” yet for another year.

#howto #programming #swiftui #swiftui4 #hacks #workarounds

If you're curious or confused by all the hype about the Twitter-like social network called Mastodon, but all the jargon and tech lingo are giving you cold feet about trying it out, this three-point guide is all you need to get started.

Just like you don't have to be a mechanical engineer to drive your car, you don't need a tech degree to use Mastodon. If you've used email, Facebook groups, or Twitter, you are 1/3 to 3/3 of the way to understanding Mastodon.

It's Similar To Email

Maybe you're alice@aol.com, or bob@gmail, or charlie@yahoo.com. You're that person who signed up for an email account from one company years ago but you don't think twice about sending mail to your friends whose addresses have a different company after the @-symbol than you.

Mastodon works in a similar fashion. Your Mastodon “address” is your @username@server.domain. Mastodon uses an @-symbol in front of the username so that it doesn't get confused with an email address. If @alice@mastodon.social  follows @bob@home.social she will see all of Bob's Mastodon posts just like she would see Bob's emails from bob@gmail.com if he CC'd her on a group email. But remember, Mastodon and email are not the same. If Alice tries to email @bob@home.social from alice@aol.com he won't see her message.

Like email, the first step to getting started on Mastodon is to pick a community  (the text after the second @-symbol). It is also called a server or an instance. The best option is to ask your friends and family if they have a Mastodon and what community server they are on.

Because so many people are interested in Mastodon right now, many Mastodon server computers are overloaded, so if you join a large community and are frustrated by how slow it is, don't give up! Size doesn't matter for finding and following people. Smaller communities are faster for now and the community you join doesn't matter if you are looking to explore Mastodon. You can find servers and communities at  https://mastodon.help/instances/ and sort them by highest or lowest users.

It Works Like A Facebook Group

You've spent the last several years building your circle of friends and family on Facebook. Your timeline feed shows friends sharing memes, grandparents talking about grandkids, and coworkers talking about sports.

But you're also part of a choir Facebook group, or a highschool alumni group. You can post about choir practice and share it with everyone you're Facebook friends with, or you can just share it with the choir group. Mastodon works the same way in that you choose what visibility to assign to your post before you share it.

A screenshot with the mastodon post visibility settings displayed.

The different types of visibility options your Mastodon post can have.xx

Unlisted: The visibility setting that is the most like a Facebook Group is “Unlisted”. If you don't want your post to be seen by the entire internet (like the way Twitter works) you can set your post to “Unlisted” before you share it. You can also configure your account to set posts to “Unlisted” by default (or any other visibility setting). When you first sign up all posts are public by default.

Just a warning, though! The only difference between posting on Mastodon and posting to a Facebook Group is that, unlike on Facebook, anyone who follows you can also see your Unlisted posts.

Followers Only: The other Mastodon visibility setting that behaves similar to Facebook is “Followers only”. This is pretty self-explanatory and works like your regular Facebook posts posted to your timeline (not to a group). If you don't want your local Mastodon community/group to see your post, choose “Followers only”.

It's A Lot Like Twitter

Mastodon will feel pretty familiar to you if you're comfortable around Twitter. Like Twitter your Mastodon posts are public to everyone (even those people outside the community or server you joined). Also like Twitter, people can follow you without your approval, or you can lock your accoutn down and review requests to follow you and reject the ones you don't want.

The Twitter algorithm is great at recommending you random things, but if you want to tailor your feed to things that interest you, you're going to have to follow people. When you first launch Mastodon your feed will be empty! That's because you are not following anyone. You can switch the web site, or the app view to display your local community, explore hashtags (and follow them!), or open up to the firehose of all the Mastodon communities (aka the Federation).

A screenshot of Mastodon with the feed types on the right and an empty home feed on the left.

An empty Home 🙁

Unlike Twitter, there is not algorithm curating your Home feed! The Home feed only shows the stuff the people you are following share (and sometimes the stuff their followers share). Because their is no algorithm, only Boosts (think of them as Retweets) show up in other people's feeds. If you star (favorite) a Mastodon post, only the author will see that—it won't cause the starred post to show up in anyone else's feed or boost engagement.

Also, if you are coming from Twitter and want to find all the people you were following there who are also on Mastodon, check out Debirdify.

Was That Too Confusing?

I'm not going to lie, Mastodon is still quite different from what you're used to and adding followers to your feed can be clunky at times. I often copy and paste the account name (Eg: @user@server) into the Mastodon search field which then gives me a follow icon. But things will get easier. After all people had a hard time getting used to email in the 90s and Facebook in the 2000s and Twitter in the 2010s.

Searching for users also displays a follow button on the right side of the results.

You will figure it out! Go sign up for Mastodon and explore. Give it some time and enjoy the community. I recommend typing #introduction into the search field. That hashtag will display lots of new, interesting Mastodon users introducing themselves and sharing tips.

Once you get some experience under your belt, check out https://mastodon.help for more advanced concepts and settings, and if your feed is empty, follow me at @quigs@hachyderm.io! It won't be empty for long!

#technology #internetculture #howto

Scott Feeney on his blog post, “Four Twitter Features Mastodon Is Better For Not Having” points out several negative aspects of Twitter's design that the Twitter-like Mastodon does not have:

If you’ve gotten in an intense argument on Twitter, you might dread this “So-and-so liked 10 Tweets you’re mentioned in” notification. It tends to mean someone went through and gave kudos to every person arguing with and insulting you. It’s stressful to be pinged over and over with this—which is exactly why we’re all tempted to do this to people we think crossed a line. In doing so, we, again, increase the reach of content we dislike.

The other features he mentions are quite refreshing too.

Also, as of today Mastodon has over 1 million active users! 👀

Eugen 💀 (@Gargron@mastodon.social)

Hey, so, we’ve hit 1,028,362 monthly active users across the network today. 1,124 new Mastodon servers since Oct 27, and 489,003 new users. That’s pretty cool.

#technology #internetculture #crosspromotion #opinion #howto