Making an editable Label with SwiftUI

Let’s say you’re showing the user a list of sidebar selectable categories. These categories appear on the sidebar as SwiftUI Labels and, depending on the one selected, the app will show one view or another.

Screen Shot 2022 01 05 at 12 45 24 PM

So far, so good… But what if the user would like to rename one of the categories?

Ideally Label would have a property to allow us to put it in edit mode. However, as of Jan 2022, that property does not exist.

With that in mind, we could pursue one of several options:

     a) Linking to an AppKit / UIKit library

     b) Using alerts

     c) Implementing it using a combination of SwiftUI classes

I went with the latter, as it seemed the cleanest approach. Here’s the result I came up with: 

QuickDemoEditLabel

In a nutshell: it’s a view that by default shows a Label and, when ‘edit mode’ is turned on, becomes a TextView. When the user is done, which is detected by editChanged and commit arguments, changes are saved and the view reverts back to a Label. 

Here’s the code:

struct EditableLabel: View{

    @ObservedObject var tag: Tag

    @State private var editing = false

    @State private var newName: String

 

    init(tag: Tag){

        self.newName = tag.name.bound

        self.tag = tag

    }

    

    var body: some View{

        if editing == true{

            TextField(“”, text: $newName, onEditingChanged: {editChanged in

                if editChanged == true{

                    //Focused

                }else{

                    //Lost focus, catches return or when user taps away

                    editing = false

                    tag.name = newName

                }

            }, onCommit: {

                //Catches escape

                editing = false

                tag.name = newName

            })

        }else{

            Label(tag.name ?? “”, systemImage: “tag”)

            .contextMenu {

                Button{

                    editing = true

                } label: {

                    Label(“Rename Tag”, systemImage: “tag”)

                }

 

            }

        }

    }

}

 

In the above example the view is connected to a CoreData class, same approach could be used with tweaks to link it to any other model. 

Thoughts / suggestions? You can reach me @MarcMasVi

Until next time, 

Marc

Iterating on MarsManaged Palette & Icon – First Round

With MarsManaged edging closer to feature-complete, today I took some time to start iterating over color palette & icon concepts. 

First, lets talk about the palette:

The premise of the app is that you’ll be as productive as future martian generations will be. With that in mind, when it comes to the color palette of the app, I wanted to start with something Blade-Runner like… 

Thumb 1920 870886

That gives me a combination or oranges and dark grays to work with. There will be cascading problems from this decision, but that’s for another post, for now let’s move right along. 

Designing the first icon:

For inspiration I really liked the image of someone looking over a planet. Something like this:

Iu

From there I dusted off Cheetah3D and PixelMator Pro and got to work on putting something quick & dirty to start with. 

First was the spaceship, a very basic spaceship with some lighting effects would do the trick. The key here, given we’re testing ideas, is to not spend too much time. 

Screen Shot 2021 12 11 at 12 32 19 PM

I’m adding some lighting and reflection so it looks like its orbiting a planet with an orangish atmosphere.

Screen Shot 2021 12 11 at 12 34 31 PM

From there, in Pixelmator I created background using a gradient and some effects to make it look like a planet surface (I settled on something between Mars and the Sun)…

Screen Shot 2021 12 11 at 1 51 48 PM

And then I added the spaceship on top of it.

Screen Shot 2021 12 11 at 12 36 34 PM

And that resulted in the first rough iteration of the icon.

At this early stage it’s all about testing ideas and iterating quickly.  

Screen Shot 2021 12 11 at 1 00 53 PM

I’m on the fence between going for something fun, using renders, or going for a more serious/flat design…  

Screen Shot 2021 12 11 at 1 32 32 PM

Given the target audience, I’m thinking it should be the latter or a combination of both.

Will take a couple days to think it through and start on the next iteration, let me know your thoughts / suggestions @MarcMasVi

Until next time, 

Marc

Looking back at 3 years of NewsWave

With its retirement approaching, here’s some trivia about NewsWave: 

AppStoreIntro

ONBOARDING 

– 70% of users that downloaded the app ended up creating an account. That was a huge increase from the 20% I was seeing initially. Below the new onboarding flow that premiered with 2020.1 -old icon included-.

Header202010

FEEDS

– NewsWave continuously monitored 4,382 feeds for updates.  

Top followed feeds among users were:

 

| ORDER  | NAME                      | URL                    |

 

|    001 | BBC News                  | bbc.co.uk              |

|    002 | CNN Top Stories           | cnn.com                |

|    003 | The New York Times        | nytimes.com            |

|    004 | NPR                       | npr.org                |

|    005 | Reuters: Top News         | reuters.com            |

|    006 | HuffPost – Front Page     | huffpost.com           |

|    007 | 9to5Mac                   | 9to5mac.com            |

|    008 | The Guardian              | theguardian.com        |

|    009 | MacRumors                 | macrumors.com          |

|    010 | Wall Street Journal       | wsj.com                |

|    011 | Washington Post – World   | washingtonpost.com     |

|    012 | TIME | Top Stories        | time.com               |

|    013 | Engadget                  | engadget.com           |

|    014 | Wired                     | wired.com              |

|    015 | BBC Science & Environment | bbc.co.uk              |

|    016 | TED Talks                 | ted.com                |

|    017 | CBC | Top Stories         | cbc.ca                 |

|    018 | Business Insider          | businessinsider.com    |

|    019 | Science Magazine          | science.sciencemag.org |

|    020 | NASA                      | nasa.gov               |

 

– It would take the server around 600 seconds to crawl & fully refresh all feeds.

– Metadata provided by RSS feeds can’t be trusted, to work around it NewsWave servers used a hash to track changes

– A de-duplication check before any article was added to the database ensured similar articles, with minor title changes or description tweaks, would not be sent again to users. 

– If enough people would subscribe to a feed, the server would then make that feed discoverable by search to other users. That’s great, right? Well, except for adult content…The good news is NewsWave already launched with an explicit content detection algorithm. The bad news is I found myself updating it quite regularly to ensure it caught all variants.  

BOOKMARKS

The average user had 12 bookmarks, in line with my expectations when I designed the system. But there were outliers at >1,500 bookmarked items, leading to some unforeseen synchronization complexities. 

…how much time should you invest on accounting for a use case that will only few users need vs. working on what the majority of users benefit from? Finding the balance is quite tricky, but I’m pleased with the mix achieved. 

USERS

At the time of this writing, NewsWave had 1,955 users and a total of 2,105 devices. The vast majority of users either used the macOS app or the iOS app, the split was:

iPhone: 47%

macOS: 41%

iPad: 12%

REVENUE MODEL

NewsWave had two tiers: a free tier and a premium tier. The objective was to make the app available to as many people as possible, thus the Free Tier aimed for a great experience with minimal server requirements while the premium one added features that had higher server costs. 

Free Tier: Great for users that only had one device, allowed up to 24 fetches/day. 

Premium Tier: Provided unlimited daily fetches and synchronization between any number of devices. 

In retrospective, differentiation between both tiers was not significant enough to motivate most users to move up to the Premium Tier. I had originally planned to show some non-intrusive ads to free users -in a very similar fashion to how Overcast does it– but I never felt NewsWave had enough users to justify all the development it would require. 

KEY NEWSWAVE POSTS (SUNRISE TO SUNSET)

– May 2019 | NewsWave Released

– June 2019 | NewsWave 2019.6 plans

– July 2019 | NewsWave 2019.6 progress

– July 2019 | NewsWave 2019.7

– October 2019 | NewsWave iOS13 Feature Update

– Feb 2020 | 2020 Roadmap

– March 2020 | NewsWave 2020.1

– March 2020 | Designing a new NewsWave Icon

– May 2020 | Announcing NewsWave for Mac

– July 2020 | NewsWave 2020.3 for Mac

– January 2021 | NewsWave for Mac 2021.01

– March 2021 | NewsWave 2021.5 for Mac & iOS

– October 2021 | NewsWave for Mac 2022.0

– November 2021 | NewsWave will be sunset early next year

CLOSING

NewsWave is the app I’m most proud of to date, not only because of the iOS and macOS apps, but also because of the server-side components: php, python, sql, Debian… I had a blast learning & building the crawler engine, the synchronization logic, the maintenance scripts… 

Ultimately thought, it was its time

Next post will be about the future, very excited about the new apps & services in the pipeline.

Marc

NewsWave will be sunset early next year

NewsWave, the twitter-style RSS news feed, will be sunset early next year.

AppStoreIntro

WHY? 

Even though it has very loyal users and both iOS and macOS apps reviewed very well -4.8 for iOS & 4.5 for macOS-, ultimately the recurrent revenue did not justify the time I’d need to invest in to further enhance it. 

Leaving it for sale while it languishes is not something I feel good with, and would not like it as a user either, so I’ve decided to sunset it now that its still working very well. 

WHAT IS CHANGING? 

NewsWave is no longer for sale on the App Store and it is no longer possible to become a premium user.

Existing users however can continue to use the app normally until July 2022, and paid subscribers can continue to use it until their premium subscription expires (subscriptions will not auto-renew). 

HOW DO I MIGRATE MY DATA?

First step is to extract the list of feeds you’re subscribed to, you do that differently depending on the app you’re using:

– macOS: Feed -> Export OPML

Screen Shot 2021 11 22 at 10 53 59 AM

– iOS: Settings -> Export OPML

IMG C1611D36FD34 1

That will generate a file that you can then proceed to import in your RSS app of choice. There’s many options, I personally like NetNewsWire as it leverages the latest Apple technologies -like CloudKit-, is available on iOS and macOS and is crazy fast. 

If you ran into any issues during the export process please do not hesitate to drop me a line at contact@mmvsolucions.com 

If you’ve been a user of the app, thank you.  To date, I’ve never worked in an app with such an engaging userbase. Tons of great feedback and always done in a constructive way, much of it translated into app improvements too! This goes to show the type of users NewsWave has had 🙂 Its been a truly rewarding side-project to work on.  

It’s always hard to sunset an app you’ve worked so hard on, at the same time it’s exciting as this frees my capacity to work on new projects. 

I’m considering a follow up post about some of the insights and learnings about NewsWave, let me know if that’s something you’d be interested in reading. 

Until next time, 

Marc

Using NSTextView in SwiftUI

MarsManaged is built on SwiftUI. At the time of this writing, November 2021, SwiftUI is still quite new and catching up to AppKit/UIKit capabilities.

As many early adopters, I found myself in a situation where a key feature I wanted to build for the app was not feasible using only SwiftUI views. I needed good old AppKit.

Thankfully, there’s a way to bridge AppKit & UIKit to SwiftUI so you can use them in your app. Unfortunately, there’s not many great examples out there.

So, I went back to WWDC, digested available documentation and tried to create the simplest template I could come up with to make it easy to adapt it to my/your app needs. In this case I’m bridging NSTextView. 

You can grab it from GitHub here, pasting it also below for your reference:

//

//  MacEditorv2.swift

//

//  Created by Marc Maset – 2021

//  https://bluelemonbits.com

//  https://twitter.com/MarcMasVi

//

import SwiftUI

 

struct MacEditorv2: NSViewRepresentable {

    func makeCoordinator() -> Coordinator {

        Coordinator(self)

    }

    

    var theTextView = NSTextView.scrollableTextView()

    

    @Binding var text: String

    

    func makeNSView(context: Context) -> NSScrollView {

        let textView = (theTextView.documentView as! NSTextView)

        textView.delegate = context.coordinator

        textView.string = text

        

        

        return theTextView

    }

    

    func updateNSView(_ nsView: NSScrollView, context: Context) {

 

    }

    

    

}

 

extension MacEditorv2{

    

    class Coordinator: NSObject, NSTextViewDelegate{

        

        var parent: MacEditorv2

        var affectedCharRange: NSRange?

        

        init(_ parent: MacEditorv2) {

            self.parent = parent

        }

        

        func textDidChange(_ notification: Notification) {

            guard let textView = notification.object as? NSTextView else {

                return

            }

            

            //Update text

            self.parent.text = textView.string

        }

        

        func textView(_ textView: NSTextView, shouldChangeTextIn affectedCharRange: NSRange, replacementString: String?) -> Bool {

            return true

        }

        

    }

    

    

}

 

Hope it helps others. Given its simplicity it should be easy to adopt it to other AppKit / UIKit classes. 

Until next time. Questions / comments? I’m @MarcMasVi

Marc

PS. unnamedd posted a great example a while back, unfortunately is not supporting newer SwiftUI features and was a bit complex to tweak. Still a great read if you’re trying to bridge to AppKit/UIKit. 

MenuBar Items in SwiftUI

Before SwiftUI you would easily add, change or remove menuBarItems from your app Storyboard.

Screen Shot 2021 10 31 at 1 14 34 PM

With the transition to SwiftUI however, there’s no longer a StoryBoard file to edit… So, what now?

After the initial surprise, and having spent a few hours reading documentation… It’s actually a very straightforward three step process:

1. In your @main add “.commands” after WindowGroup 

WindowGroup {

            […]

           

        }.commands {

            

           //Here well add the MenuBarItems

            

        }

2. Then you have a couple choices: you can replace existing CommandGroups (like File-> New), enhance them with more options, or create a new MenuBar groups altogether. 

First, lets look at how you would create new MenuItems:

CommandMenu(“New menu”) {

    Button(“Some message”) {

        print(“Hello World!”)

    }.keyboardShortcut(d”)

 

    Button(“Second message”) {

        print(“Hello World2!”)

    }

 

    Picker(selection: $selection, label: Text(Selection”)) {

        Text(“Option 1”).tag(1)

        Text(“Option 2”).tag(2)

        Text(“Option 3”).tag(3)

    }

}

Yes, you can even create a picker!

But wait, if instead you’d like to replace or add to an existing menu:

CommandGroup(before: CommandGroupPlacement.newItem) {

        Button(“Something”) {

           //

        }

    }

 

    CommandGroup(replacingCommandGroupPlacement.newItem) {

        Button(“Something”) {

            // 

        }

    }

 

    CommandGroup(afterCommandGroupPlacement.newItem) {

        Button(Something”) {

           //

        }

}

 

Finally, what about cases where you’d like to trigger complex actions from a MenuBarItem? Well, you can create a SwiftUI class for it. 

Here’s how you would use the classes in the .commands section:

}.commands {

            //Check if we’re in task menu

            NewItem(ccd: ccd, viewContext: persistenceController.container.viewContext)

            DeleteItem(ccd: ccd, viewContext: persistenceController.container.viewContext)

            PinElement(ccd: ccd, viewContext: persistenceController.container.viewContext)

            Help()

            

        }

And here’s the contents of one of them for reference, in this case replacing the help menu with my custom buttons:

 

import SwiftUI

 

struct Help: Commands {

    var body: some Commands {

        CommandGroup(replacing: .help, addition: {

            Button(“Help”){

                let helpLink = “http://bluelemonbits.com”

                NSWorkspace.shared.open(URL(string: helpLink)!)

            }

            Button(“Contact us”){

                let service = NSSharingService(named: NSSharingService.Name.composeEmail)!

                service.recipients = [x@x.com”]

                service.subject = “Feedback \(arc4random() % 100000)

                service.perform(withItems: [” \nFeedback: \n”])

                

            }

        })

    }

}

 

Hope it helps! Questions/feedback? I’m @MarcMasVi

Until next time, 

 

Marc

 

PS: Some related articles on this very topic

Preventing edits in TextFields unless cell is selected

For the ToDo section in MarsManaged, the goal is for the user to see and quickly manage their upcoming tasks. 

Screen Shot 2021 10 30 at 6 09 57 PM

The user should be able to easily tweak bookmarked tasks, set target dates, complete or delete them.

Now, given that each cell is composed of multiple editable TextFields, it’s important to prevent unintended editing on cells that are not currently selected.

Otherwise the user inadvertently could:

– Edit text in a cell that’s not selected.

– When attempting to select a cell, if the click lands on text, they would directly start editing it -even though the cell would not be selected-.

– Right click on the TextField would result in all text being selected and wrong context menu options being shown. 

Screen Shot 2021 10 30 at 6 16 43 PM

For instance, in the image above the second cell is selected but the user can click in TextField 1 from the first cell and start editing -while the second cell is still selected-.

So, how can we ensure only the selected cell is editable? Well, there’s two options:

a) You can add a modifier to make cells disabled when they are not selected:

TextField(Placeholder…”, text: $task.content.boundAndClean, onCommit: {

         .disabled(selectedTaskId! != task.id)

}

 

b) You can add a different modifier to only register clicks when the cell is selected.  

TextField(Placeholder…”, text: $task.content.boundAndClean, onCommit: {

          .allowsHitTesting(selectedTaskId! == task.id)

 

}

Important to note is that if you disable the TextField, the text will appear in a different color. However, allowsHitTesting does not affect color at all. 

This closes yet another item on my list for MarsManaged. It’ll when ship when its ready, but I expect to start early testing in the next couple months. If interested & are running macOS Monterey drop me a line at contact@mmvsolucions.com

Until next time, 

 

Marc

SwiftUI modifier order matters

This Sunday I spent pretty much all day working on MarsBased.

I woke up early, went to the gym, made myself some coffee and got to work. 

Screen Shot 2021 10 18 at 8 45 12 PM

In front of me was a prioritized list of “feature enhancements” my wife had prepared for me -she’s an excellent Product Manager so she always has great feedback- after testing the app a day early. 

I started going through the list… First couple of suggestions were icon changes which I dispatched in about 5m. Off to a great start. 

Now I reach what is supposed to be an extremely easy one: add contextMenu to the TODO tasks. In essence, for each task you should be able to right click and you’d see a couple options: delete, focus, complete… Easy right? Right?

Well, should have been. Turns out when I added the contextMenu, selection stopped working… You could right click, and would see the options, but normal selection would not work anymore.

Screen Shot 2021 10 18 at 8 51 10 PM

Then I realized -after much, much, much searching and try and error-, that selection would work again if instead of using a Int as the tag I would use an Int64. Why you ask? Excellent question, absolutely no idea.

Solved right? Well.. No

Now the challenge was @SceneStorage would not work with Int64. I could make it work with @State mind you, but not with @SceneStorage… And most importantly, I did not know what was going on which made this approach -even if it were to work (which it did not)- a no go. 

At this point I had been at least four hours working on this and there was no solution in sight…

In the end, I decided not to waste more time. I was stuck, and have enough experience to know (usually) when to stop. 

Went for a walk, relaxed in the garden and went to sleep. Today, after work I decided to take another crack at it. This time I tried a different approach.

I created a new SwiftUI file and started from scratch, testing different combinations of ForEach or Lists. And what do you know, it worked on the first try… Wait what? I compared the code, it was essentially the same, what’s was going on there… 

OH… I… SEE…

If you use a tag modifier before the contextMenu, selection will stop working. If you first have the contextMenu modifier and then the tag modifier it works great… Good to know. 

So that’s it, about 6 hours of my life debugging something that turned out to be the order of modifiers. 

Learn from my misakes, modifier order matters 🙂 

Until next time, 

Marc