My First App - Grow (Journey from Scratch!)

I hear you, thanks for your suggestions! The spacing above my title on the “Journey” screen won’t be such a big white space, It’s just avoiding the camera bezel which isn’t shown on the design frame. As for the spacing between the “Have a Mac” title and the card, I see what you mean and this might indeed be confusing, I’ll make the spacing smaller :+1:. Originally I put the spacing there to show that the title “Have a Mac” is a subgoal to accomplish and the card contains the plan to achieve it. But it might be better to let go of this idea to avoid confusion.

I’ll let you know if I have a solution for avoiding keyboard! I’m currently fiddling with some code to find a solution.

Indeed, humility comes before a ris, nice saying! And I also think that a simpler design can communicate your idea more effective. Since there is less confusion and less to see, the user’s eye is guided to the right elements and can comprehend and admire the design easier.

Thanks!

Have a good afternoon as well!
-Rune

Update 05/09/2024

First off, I just want to say I’ve had some trouble staying motivated recently; especially with the school year coming to an end and my mind slowly drifting away into the summer relaxation mode (which only lasts until I get a job). :relieved: So, consequently, progress has been slow-going.

Anyways, that’s not to say I haven’t made any progress; most of it has been brainstorming and contemplation. I want to share a sheet I filled out from Apple’s App Workbook (thanks Rune for the suggestion :slightly_smiling_face:); you can find it in the attachments. The major difficulty I am facing right now is how to research my potential users and gather user personas. For several reasons, I do not have a social media account, so it makes things a little difficult in terms of outreach, but I’m sure there are some valid solutions to this.

Excuse me for reiterating this (it’s just I’m gaining a clearer understanding as time goes on): my app is primarily targeted at people who want to learn how to focus on a device that controversially is meant to keep people “multitasking" all the time, and at people who want to free themselves from technology a little bit in general. Obviously, technology has many benefits, so how can we focus on these and get rid of its problems? (This is just a question I’m asking myself as part of my app brainstorming.)

Overall, this seems like an important issue to address. If you think about it, companies profit greatly from users using their devices constantly because say a user downloads Facebook; then they see an add for Spotify and download that too; then they realize they are spending way too much time on their phones and they (ironically) download a focus app… It goes on and on. Okay, maybe that was too dramatic, but I think that conveys my meaning pretty well: technology is often abused. As Steve Jobs said, "Technology is nothing. What’s important is that you have faith in people, that they’re basically good and smart — and if you give them tools, they’ll do wonderful things with them.” So, technology is supposed to be for the people, not the people for technology.

What does this mean for my app, then? Should it be just another app that tries to draw the user in, or should it respect the human being using it and try to give him/her the freedom to use the app by not using it? These are all questions that I am trying to narrow down into a single app idea as a MVP at least for now.

Thanks for reading! Now anyone looking through this will have a better idea of how I think about app creation in its initial stages. I truly enjoy brainstorming my ideas with other people, so thanks for indulging me. :smile:

-Michael

Sounds good! That makes sense. I was guessing there was a reason behind it.

Yes! Please do. There will always be little quirks here and there when coding a new app. Besides, SwiftUI is still rather new.

I so agree with you on the design points.

Talk to you later,
-Michael

Hello Michael,

Great news! I think I’ve found a solution on avoiding the keyboard.
When you want to avoid the keyboard in SwiftUI, you have two options:

  1. Build a custom class, modifier or API to detect and do all the calculations.
  2. Let SwiftUI handle the calculations and just guide SwiftUI to suit your app.

Having experience trying these two options, I’d choose the second option as SwiftUI never calculates wrong, and why should we go through the hassle of building our own code when Apple engineers have prepared one for us?
So we’ll use SwiftUI and just guide it to suit our own app. First I’ll explain how SwiftUI’s keyboard avoidance works. When the keyboard shows, SwiftUI simply increases the bottom safe area to the size of the keyboard, so your view is compressed into the visible space above the keyboard.
This is also why you can find the modifier to ignore this feature as follows:
.ignoresSafeArea(.keyboard)
Note: this only works when your view has enough compressible space (by which I mean view objects like Spacer, Rectangle, …), to show all its content in the space above the keyboard, when your view has to much absolute space, SwiftUI will ignore this modifier.

Taking this into account, we can again choose between two approaches, when you have compressible space in your view layout, you can safely let SwiftUI do its thing, just keep in mind that your view will be compressed into the smaller space above the keyboard when it shows. The only headache with this approach is that there is no direct way of specifying the spacing between two objects, or I didn’t find one :sweat_smile:. And this is where my solution comes into play, I’ve created a custom modifier which lets you specify a maximum frame which the compressible view object can occupy and a minimum frame to which this object can be compressed. The modifier uses SwiftUI’s Layout System to achieve this. Here is my code:

import SwiftUI

struct LimitFrame: Layout, ViewModifier {
    
    var maxWidth: CGFloat?
    var maxHeight: CGFloat?
    var minWidth: CGFloat?
    var minHeight: CGFloat?
    var alignment: Alignment
    
    func body(content: Content) -> some View {
        LimitFrame(maxWidth: maxWidth, maxHeight: maxHeight, minWidth: minWidth, minHeight: minHeight, alignment: alignment) {
            content
        }
    }
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        guard subviews.count == 1, let content = subviews.first else {
            fatalError("LimitFrame can't have more than 1 subview.")
        }
        var result: CGSize = .zero
        
        // Define proposal with given max values
        let proposalWidth = numberBetween(max: maxWidth, min: minWidth, value: proposal.width ?? proposal.replacingUnspecifiedDimensions().width)
        let proposalHeight = numberBetween(max: maxHeight, min: minHeight, value: proposal.height ?? proposal.replacingUnspecifiedDimensions().height)
        let newProposal: ProposedViewSize = .init(width: proposalWidth, height: proposalHeight)
        
        // Ask required size
        let requiredSize = content.sizeThatFits(newProposal)
        
        // Compare required size with given values
        let width: CGFloat = numberBetween(max: maxWidth, min: minWidth, value: requiredSize.width)
        let height: CGFloat = numberBetween(max: maxHeight, min: minHeight, value: requiredSize.height)
        result = .init(width: width, height: height)
        
        // Return result
        return result
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        guard subviews.count == 1, let content = subviews.first else {
            fatalError("LimitFrame can't have more than 1 subview.")
        }
        
        // Calculate leading and top constraints
        let dimensions = content.dimensions(in: .init(width: bounds.width, height: bounds.height))
        var xOffset: CGFloat {
            switch alignment.horizontal {
            case .leading:
                return 0
            case .trailing:
                return bounds.width
            default:
                return bounds.width / 2
            }
        }
        var yOffset: CGFloat {
            switch alignment.vertical {
            case.top:
                return 0
            case .bottom:
                return bounds.height
            default:
                return bounds.height / 2
            }
        }
        
        let leading = bounds.minX + xOffset - dimensions[alignment.horizontal]
        let top = bounds.minY + yOffset - dimensions[alignment.vertical]
        
        // Place the content at that position with the bounds as its proposed size
        content.place(at: .init(x: leading, y: top), anchor: .topLeading, proposal: .init(width: bounds.width, height: bounds.height))
    }
    
    func numberBetween(max maxValue: CGFloat?, min minValue: CGFloat?, value: CGFloat) -> CGFloat {
        let maxLimit = maxValue ?? .infinity
        let minLimit = minValue ?? .zero
        
        return min(maxLimit, max(value , minLimit))
    }
}

extension View {
    /// Limits the frame to be within the specified range.
    func limitFrame(maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil, minWidth: CGFloat? = nil, minHeight: CGFloat? = nil, alignment: Alignment = .center) -> some View {
        modifier(LimitFrame(maxWidth: maxWidth, maxHeight: maxHeight, minWidth: minWidth, minHeight: minHeight, alignment: alignment))
    }
    
    @available(*, deprecated, message: "Please specify at least one parameter other than alignment.")
    func limitFrame(alignment: Alignment = .center) -> some View {
        modifier(LimitFrame(alignment: alignment))
    }
}

If you’d like to learn more about the layout system in SwiftUI, you can watch this video by Paul Hudson, or if you’d like I’d also be happy to break it down for you.

We can also still use the second option, which you’d use when you can’t use or don’t want to use compressible space. You can put your view inside a ScrollView, and SwiftUI will automatically scroll to where the focused text field is shown. This works because ScrollView works with 2 layers, the first layer which it uses to position itself between other view objects, and the second layer in which it renders its child views. The second layer isn’t limited by the bounds of the first layer and therefore won’t be compressed when the keyboard shows, only the first layer will be compressed.

Hope this might help you on a later date, and if you don’t understand something please do say so!

Greetings, Rune

Thank you! This is incredible! Okay, I see how this works. When I first heard about keyboard avoidance (the problem you were having earlier), I had to look up what it meant and this helps clarify those assumptions. Thanks for explaining everything so thoroughly.

I see you found a self-coded solution to the problem! I can understand most of the syntax, but am not quite sure what each piece of code does in the overall structure of the keyboard avoidance mechanism. Perhaps you could expand on the purpose of each func? I’m just curious about the role that each function plays in context. (I’ll plan to watch the video too when I get the chance.) :slight_smile:

It makes sense that a ScrollView is one of the most effective ways to avoid compression of the onscreen elements. It’s good to know the different solutions. Interesting layers situation! It makes sense, though.

Have a great day!

-Michael

Of course! I’ll first explain how the layout system in SwiftUI works and then move on to my code, this may provide a useful background.

SwiftUI is works with declarative view code, meaning your code represents the view layout. This is handy because it provides a better overview of how your view looks like and it is easier to learn. This also means that SwiftUI will read your view code and do all tricky calculations of placements and sizing etc. itself. Normally this is great and we don’t have to worry about it, but in this situation we need to understand how SwiftUI approaches this.
The direction in which SwiftUI reads your view code is from parent/container view to child views, it passes a proposed size to the view container in question, which then passes that size to its child view and asks to calculate its own size using the proposed size. The child view isn’t obligated to consider the proposed size when calculating its own size. When the container view got returned the size from the child view, it adapts its own size to hug the size of its child view, and then returns its own size to SwiftUI. This is how SwiftUI determines sizing. We can put this process in a few steps:

  1. Parent/container view receives proposed size.
  2. Parent/container view subtracts any specified spacing from this and passes the result as proposed size to its child view(s) using its sizeThatFits function.
  3. Child view calculates its own size in its sizeThatFits function whether or not taking the proposed size into account and returns its required size.
  4. Parent/container view takes the required size of its child view(s) and adapts it own size to it (also taking any specified spacing into account), and returns its own required size.
  5. SwiftUI uses these sizes to place the views in the available space calling the placeSubviews function on the parent view.

Taking all that into account, we can alter the two mentioned functions by conforming to the Layout protocol.

In the sizeThatFits function, where proposal is the proposed size, subviews a list of each subview in our parent view and cache a way of remembering the result of previous calculations to improve performance, we just check that the proposed size is between the given limit and pass that to our subviews as their proposed size. After the child view has returned its required size, we check again if this size is between the given limit and return it.
Note: The struct is defined as a container in its body, but since I use it as a view modifier, I make sure that there is only one child view.

In the placeSubviews, where bounds is the size for the container, proposal the previously proposed size and subviews and cache serve the same purposes as the sizeThatFits function, I simply place the views with the given bounds. I calculated the position of the child view using the given alignment. This involves some knowledge about Alignment Guides and how SwiftUI works with it, but you don’t have to worry about this as it isn’t relevant to the Layout System in SwiftUI.

I know this may be a bit much info at once, but I hope you could follow my explanation. Should you have any more questions, feel free to ask me.

Have a great evening,
-Rune

Thank you so much! This definitely makes a lot of sense. It’s cool to “look behind the scenes” at the very intentional software engineering that went into SwiftUI. I’ll certainly come back to this when designing my app, so thanks for explaining it.

May you also have a great evening.

-Michael :wink:

Update 05/16/2024

So, I have made several important decisions this past week:

First, after reading a lot of user reviews on the App Store, I decided to utilize many user-appreciated features included in successful focus apps; this has provided me with a solid foundation for what concepts I want to employ now and in the future. These concepts include, but are not limited to, the notification concept, the focus concept (speaking specifically about app blocking and screen time), the to-do concept, and the the progress concept (as seen in the Lotus Flower). Some additional concepts that I might use post-MVP stage are the alternatives concept (basically giving the user suggestions for how they can complete a task without using their device), the AI concept (a lot can be said here), the iCloud concept, and the collaboration concept (basically sharing your rewards with your friends and completing challenges together).

Second, I am now starting to create the designs for the different UI elements in my app. Like my architecture teacher taught me to do, I am first working on the individual pieces before combining them into an entire building (or app interface). The general rule of thumb is Form Follows Function, that way the UX is given priority over the UI, I guess. There is an attachment showing my designs (these are basically all the app elements I have created for Grow). In the final design, I will probably change the colors around a little bit so that everything looks good in dark mode.

Thirdly, by reading through the user reviews, I have also come across terms like ADHD and procrastinating, so that has helped me learn more about the user-side of all this. Many users seem to praise collaborative features and are also (understandably) quite frustrated by an app riddled with bugs. If I can release just a few new features at a time, perhaps I can avoid releasing an app with lots of bugs. There is definitely room for improvement when it comes to the focus apps I have looked into.

Next week I will be a little busy, but I will try my best to make more progress on my app idea.

Have a nice evening!

-Michael :slight_smile:

No problem! Glad I could help.