Tag: swift

  • Discover App Features with TipKit

    In today’s digital age, the user experience is paramount. Mobile applications need to be intuitive and user-friendly so that users not only enjoy the app’s main functionalities but also easily navigate through its features. There are instances where a little extra guidance can go a long way, whether it’s introducing users to a fresh feature, showing them shortcuts to complete tasks more efficiently, or simply offering tips on getting the most out of an app. Many developers have traditionally crafted custom overlays or tooltips to bridge this gap, often requiring a considerable amount of effort. But the wait for a streamlined solution is over. After much anticipation, Apple has introduced the TipKit framework, a dedicated tool to simplify this endeavor, enhancing user experience with finesse.

    TipKit

    Introduced at WWDC 2023, TipKit emerges as a beacon for app developers aiming to enhance user engagement and experience. This framework is ingeniously crafted to present mini-tutorials, shining a spotlight on new, intriguing, or yet-to-be-discovered features within an application. Its utility isn’t just confined to a single platform—TipKit boasts integration with iCloud to ensure data synchronization across various devices.

    At the heart of TipKit lies its two cornerstone components: the Tip Protocol and the TipView. These components serve as the foundation, enabling developers to craft intuitive and informative tips that resonate with their user base.

    Tip Protocol

    The essence of TipKit lies in its Tip Protocol, which acts as the blueprint for crafting and configuring content-driven tips. To create your tips tailored to your application’s needs, it’s imperative to conform to the Tip Protocol.

    While every Tip demands a title for identification, the protocol offers flexibility by introducing a suite of properties that can be optionally integrated, allowing developers to craft a comprehensive and informative tip.

    1. title(Text): The title of the Tip.
    2. message(Text): A concise description further elaborates the essence of the Tip, providing users with a deeper understanding.
    3. asset(Image): An image to display on the left side of the Tip view.
    4. id(String): A unique identifier to your tip. Default will be the name of the type that conforms to the Tip protocol.
    5. rules(Array of type Tips.Rule): This can be used to add rules to the Tip that can determine when the Tip needs to be displayed.
    6. options(Array of type Tips.Option): Allows to add options for defining the behavior of the Tip.
    7. actions(Array of type Tips.Action): This will provide primary and secondary buttons in the TipView that could help the user learn more about the Tip or execute a custom action when the user interacts with it.

    Creating a Custom Tip

    Let’s create our first Tip. Here, we are going to show a Tip to help the user understand the functionality of the cart button.

    struct CartItemsTip: Tip {
        var title: Text {
            Text("Click the cart button to see what's in your cart")
        }
        var message: Text? {
            Text("You can edit/remove the items from your cart")
        }
        var image: Image? {
            Image(systemName: "cart")
        }
    }

    TipView

    As the name suggests, TipView is a user interface that represents the Inline Tip. The initializer of TipView requires an instance of the Tip protocol we discussed above, an Edge parameter, which is optional, for deciding the edge of the tip view that displays the arrow.

    Displaying a Tip

    Following are the two ways the Tip can be displayed.

    • Inline

    You can display the tip along with other views. An object of TipView requires a type conforming Tip protocol used to display the Inline tip. As a developer, handling multiple views on the screen could be a complex and time-consuming task. TipKit framework makes it easy for the developers as it automatically adjusts the layout and the position of the TipView to ensure other views are accessible to the user. 

    struct ProductList: View {
        private let cartTip = CartItemsTip()
        var body: some View {
            \ Other views
            
            TipView(cartTip)
            
            \ Other views
        }
    }

    • Popover

    TipKit Frameworks allow you to show a popover Tip for any UI element, e.g., Button, Image, etc. The popover tip appears over the entire screen, thus blocking the other views from user interaction until the tip is dismissed. A popoverTip modifier displays a Popover Tip for any UI element. Consider an example below where a Popover tip is displayed for a cart image.

    private let cartTip = CartItemsTip()
    Button {
       cartTip.invalidate(reason: .actionPerformed)
    } label: {
       Image(systemName: "cart")
           .popoverTip(cartTip)
    }

    Dismissing the Tip

    A TipView can be dismissed in two ways.

    1. The user needs to click on X icon.
    2. Developers can dismiss the Tip programmatically using the invalidate(reason:) method.

    There are 3 options to pass as a reason for dismissing the tip: 

    actionPerformed, userClosedTip, maxDisplayCountExceeded

    private let cartTip = CartItemsTip()
    cartTip.invalidate(reason: .actionPerformed)

    Tips Center

    We have discussed essential points to define and display a tip using Tip protocol and TipView, respectively. Still, there is one last and most important step—to configure and load the tip using the configure method as described in the below example. This is mandatory to display the tips within your application. Otherwise, you will not see tips.

    import SwiftUI
    import TipKit
    
    @main
    struct TipkitDemoApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .task {
                        try? Tips.configure([
                            .displayFrequency(.immediate),
                            .datastoreLocation(.applicationDefault)
                        ])
                    }
            }
        }
    }

    If you see the definition of configure method, it should be something like:

    static func configure(@Tips.ConfigurationBuilder options: @escaping () -> some TipsConfiguration = { defaultConfiguration }) async throws

    If you notice, the configure method accepts a list of types conforming to TipsConfiguration. There are two options available for TipsConfiguration, DisplayFrequency and DataStoreLocation. 

    You can set these values as per your requirement. 

    DisplayFrequency

    DisplayFrequnecy allows you to control the frequency of your tips and has multiple options. 

    • Use the immediate option when you do not want to set any restrictions.
    • Use the hourly, daily, weekly, and monthly values to display no more than one tip hourly, weekly, and so on, respectively. 
    • For some situations, you need to set the custom display frequency as TimeInterval, when all the available options could not serve the purpose. In the below example, we have set a custom display frequency that restricts the tips to be displayed once per two days.
    let customDisplayFrequency: TimeInterval = 2 * 24 * 60 * 60
    try? Tips.configure([
         .displayFrequency(customDisplayFrequency),
         .datastoreLocation(.applicationDefault)
    ])

    DatastoreLocation

    This will be used for persisting tips and associated data. 

    You can use the following initializers to decide how to persist tips and data.

    public init(url: URL, shouldReset: Bool = false)

    url: A specific URL location where you want to persist the data.

    shouldReset: If set to true, it will erase all data from the datastore. Resetting all tips present in the application.

    public init(_ location: DatastoreLocation, shouldReset: Bool = false)

    location: A predefined datastore location. Setting a default value ‘applicationDefault’ would persist the datastore in the app’s support directory. 

    public init(groupIdentifier: String, directoryName: String? = nil, shouldReset: Bool = false) throws

    groupIdentifier: The name of the group whose shared directory is used by the group of your team’s applications. Use the optional directoryName to specify a directory within this group.

    directoryName: The optional directoryName to specify a directory within the group.

    Max Display Count

    As discussed earlier, we can set options to define tip behavior. One such option is MaxDisplayCount. Consider that you want to show CartItemsTip whenever the user is on the Home screen. Showing the tip every time a user comes to the Home screen can be annoying or frustrating. To prevent this, one of the solutions, perhaps the easiest, is using MaxDisplayCount. The other solution could be defining a Rule that determines when the tip needs to be displayed. Below is an example showcasing the use of the MaxDisplayCount option for defining CartItemsTip.

    struct CartItemsTip: Tip {
        var title: Text {
            Text("Click here to see what's in your cart")
        }
        
        var message: Text? {
            Text("You can edit/remove the items from your cart")
        }
        
        var image: Image? {
            Image(systemName: "cart")
        }
        
        var options: [TipOption] {
            [ MaxDisplayCount(2) ]
        }
    }

    Rule Based Tips

    Let’s understand how Rules can help you gain more control over displaying your tips. There are two types of Rules: parameter-based rules and event-based rules.

    Parameter Rules

    These are persistent and more useful for State and Boolean comparisons. There are Macros (#Rule, @Parameter) available to define a rule. 

    In the below example, we define a rule that checks if the value stored in static itemsInCart property is greater than or equal to 3. 

    Defining rules ensures displaying tips only when all the conditions are satisfied.

    struct CartTip: Tip {
        
        var title: Text {
            Text("Proceed with buying cart items.")
        }
        
        var message: Text? {
            Text("There are 3 or more items in your cart.")
        }
        
        var image: Image? {
            Image(systemName: "cart")
        }
        
        @Parameter
        static var itemsInCart: Int = 0
        
        var rules: [Rule] {
            #Rule(Self.$itemsInCart) { $0 >= 3 }
        }
    }

    Event Rules

    Event-based rules are useful when we want to track occurrences of certain actions in the app. Each event has a unique identifier id of type string, with which we can differentiate between various events. Whenever the action occurs, we need to use the denote() method to increment the counter. 

    Let’s consider the below example where we want to show a Tip to the user when the user selects the iPhone 14 Pro (256 GB) – Purple product more than 2 times.

    The example below creates a didViewProductDetail event with an associated donation value and donates it anytime the ProductDetailsView appears:

    struct ProductDetailsView: View {
        static let didViewProductDetail = Tips.Event<DidViewProduct>(id: "didViewProductDetail")
        var product: ProductModel
        var body: some View {
            VStack(alignment: .leading) {
                HStack(alignment: .top, content: {
                    Spacer()
                    Image(product.productImage, bundle: Bundle.main)
                    Spacer()
                })
                Text(product.productName)
                    .font(.title3)
                    .lineLimit(nil)
                Text(product.productPrice)
                    .font(.title2)
                Text("Get it by Wednesday, 18 October")
                    .font(.caption2)
                    .lineLimit(nil)
                Spacer()
            }
            .padding()
            .onAppear {
                Self.didViewProductDetail.sendDonation(.init(productID: product.productID, productName: product.productName))
                    }
        }
    }
    
    struct DidViewProduct: Codable, Sendable {
        let productID: UUID
        let productName: String
    }

    The example below creates a display rule for ProductDetailsTip based on the didViewProductDetail event.

    struct ProductDetailsTip: Tip {
        var title: Text {
            Text("Add iPhone 14 Pro (256 GB) - Purple to your cart")
        }
        
        var message: Text? {
            Text("You can edit/remove the items from your cart")
        }
        
        var image: Image? {
            Image(systemName: "cart")
        }
        
        var rules: [Rule] {
            // Tip will only display when the didViewProductDetail event for product name 'iPhone 14 Pro (256 GB) - Purple' has been donated 3 or more times in a day.
            #Rule(ProductDetailsView.didViewProductDetail) {
                $0.donations.donatedWithin(.day).filter( { $0.productName == "iPhone 14 Pro (256 GB) - Purple" }).count >= 3
            }
        }
        
        var actions: [Action] {
            [
                Tip.Action(id: "add-product-to-cart", title: "Add to cart", perform: {
                    print("Product added into the cart")
                })
            ]
        }
    }

    Customization for Tip

    Customization is the key feature as every app has its own theme throughout the application. Customizing tips to gale along with application themes surely enhances the user experience. Although, as of now, there is not much customization offered by the TipKit framework, but we expect it to get upgraded in the future. Below are the available methods for customization of tips.

    public func tipAssetSize(_ size: CGSize) -> some View
    
    public func tipCornerRadius(_ cornerRadius: Double, 
                                antialiased: Bool = true) -> some View
    
    public func tipBackground(_ style: some ShapeStyle) -> some View

    Testing

    Testing tips is very important as a small issue in the implementation of this framework can ruin your app’s user experience. We can construct UI test cases for various scenarios, and tthe following methods can be helpful to test tips.

    • showAllTips
    • hideAllTips
    • showTips([<instance-of-your-tip>])
    • hideTips([<instance-of-your-tip>])

    Pros

    • Compatibility: TipKit is compatible across all the Apple platforms, including iOS, macOs, watchOs, visionOS.
    • Supports both SwiftUI and UIKit
    • Easy implementation and testing
    • Avoiding dependency on third-party libraries

    Cons 

    • Availability: Only available from iOS 17.0, iPadOS 17.0, macOS 14.0, Mac Catalyst 17.0, tvOS 17.0, watchOS 10.0 and visionOS 1.0 Beta. So no backwards compatibility as of now.
    • It might frustrate the user if the application incorrectly implements this framework

    Conclusion

    The TipKit framework is a great way to introduce new features in our application to the user. It is easy to implement, and it enhances the user experience. Having said that, we should avoid extensive use of it as it may frustrate the user. We should always avoid displaying promotional and error messages in the form of tips.

  • Optimizing iOS Memory Usage with Instruments Xcode Tool

    Introduction

    Developing iOS applications that deliver a smooth user experience requires more than just clean code and engaging features. Efficient memory management helps ensure that your app performs well and avoids common pitfalls like crashes and excessive battery drain. 

    In this blog, we’ll explore how to optimize memory usage in your iOS app using Xcode’s powerful Instruments and other memory management tools.

    Memory Management and Usage

    Before we delve into the other aspects of memory optimization, it’s important to understand why it’s so essential:

    Memory management in iOS refers to the process of allocating and deallocating memory for objects in an iOS application to ensure efficient and reliable operation. Proper memory management prevents issues like memory leaks, crashes, and excessive memory usage, which can degrade an app’s performance and user experience. 

    Memory management in iOS primarily involves the use of Automatic Reference Counting (ARC) and understanding how to manage memory effectively.

    Here are some key concepts and techniques related to memory management in iOS:

    1. Automatic Reference Counting (ARC): ARC is a memory management technique introduced by Apple to automate memory management in Objective-C and Swift. With ARC, the compiler automatically inserts retain, release, and autorelease calls, ensuring that memory is allocated and deallocated as needed. Developers don’t need to manually manage memory by calling “retain,” “release,” or “autorelease`” methods as they did in manual memory management in pre-ARC era.
    2. Strong and Weak References: In ARC, objects have strong, weak, and unowned references. A strong reference keeps an object in memory as long as at least one strong reference to it exists. A weak reference, on the other hand, does not keep an object alive. It’s commonly used to avoid strong reference cycles (retain cycles) and potential memory leaks.
    3. Retain Cycles: A retain cycle occurs when two or more objects hold strong references to each other, creating a situation where they cannot be deallocated, even if they are no longer needed. To prevent retain cycles, you can use weak references, unowned references, or break the cycle manually by setting references to “nil” when appropriate.
    4. Avoiding Strong Reference Cycles: To avoid retain cycles, use weak references (and unowned references when appropriate) in situations where two objects reference each other. Also, consider using closure capture lists to prevent strong reference cycles when using closures.
    5. Resource Management: Memory management also includes managing other resources like files, network connections, and graphics contexts. Ensure you release or close these resources when they are no longer needed.
    6. Memory Profiling: The Memory Report in the Debug Navigator of Xcode is a tool used for monitoring and analyzing the memory usage of your iOS or macOS application during runtime. It provides valuable insights into how your app utilizes memory, helps identify memory-related issues, and allows you to optimize the application’s performance.

    Also, use tools like Instruments to profile your app’s memory usage and identify memory leaks and excessive memory consumption.

    Instruments: Your Ally for Memory Optimization

    In Xcode, “Instruments” refer to a set of performance analysis and debugging tools integrated into the Xcode development environment. These instruments are used by developers to monitor and analyze the performance of their iOS, macOS, watchOS, and tvOS applications during development and testing. Instruments help developers identify and address performance bottlenecks, memory issues, and other problems in their code.

     

    Some of the common instruments available in Xcode include:

    1. Allocations: The Allocations instrument helps you track memory allocations and deallocations in your app. It’s useful for detecting memory leaks and excessive memory usage.
    2. Leaks: The Leaks instrument finds memory leaks in your application. It can identify objects that are not properly deallocated.
    3. Time Profiler: Time Profiler helps you measure and analyze the CPU usage of your application over time. It can identify which functions or methods are consuming the most CPU resources.
    4. Custom Instruments: Xcode also allows you to create custom instruments tailored to your specific needs using the Instruments development framework.

    To use these instruments, you can run your application with profiling enabled, and then choose the instrument that best suits your performance analysis goals. 

    Launching Instruments

    Because Instruments is located inside Xcode’s app bundle, you won’t be able to find it in the Finder. 

    To launch Instruments on macOS, follow these steps:

    1. Open Xcode: Instruments is bundled with Xcode, Apple’s integrated development environment for macOS, iOS, watchOS, and tvOS app development. If you don’t have Xcode installed, you can download it from the Mac App Store or Apple’s developer website.
    2. Open Your Project: Launch Xcode and open the project for which you want to use Instruments. You can do this by selecting “File” > “Open” and then navigating to your project’s folder.
    3. Choose Instruments: Once your project is open, go to the “Xcode” menu at the top-left corner of the screen. From the drop-down menu, select “Open Developer Tool” and choose “Instruments.”
    4. Select a Template: Instruments will open, and you’ll see a window with a list of available performance templates on the left-hand side. These templates correspond to the different types of analysis you can perform. Choose the template that best matches the type of analysis you want to conduct. For example, you can select “Time Profiler” for CPU profiling or “Leaks” for memory analysis.
    5. Configure Settings: Depending on the template you selected, you may need to configure some settings or choose the target process (your app) you want to profile. These settings can typically be adjusted in the template configuration area.
    6. Start Recording: Click the red record button in the top-left corner of the Instruments window to start profiling your application. This will launch your app with the selected template and begin collecting performance data.
    7. Analyze Data: Interact with your application as you normally would to trigger the performance scenarios you want to analyze. Instruments will record data related to CPU usage, memory usage, network activity, and other aspects of your app’s performance.
    8. Stop Recording: When you’re done profiling your app, click the square “Stop” button in Instruments to stop recording data.
    9. Analyze Results: After stopping the recording, Instruments will display a detailed analysis of your app’s performance. You can explore various graphs, timelines, and reports to identify and address performance issues.
    10. Save or Share Results: You can save your Instruments session for future reference or share it with colleagues if needed.

    Using the Allocations Instrument

    The “Allocations” instrument helps you monitor memory allocation and deallocation. Here’s how to use it:

    1. Start the Allocations Instrument: In Instruments, select “Allocations” as your instrument.

    2. Profile Your App: Use your app as you normally would to trigger the scenarios you want to profile.

    3. Examine the Memory Allocation Graph: The graph displays memory usage over time. Look for spikes or steady increases in memory usage.

    4. Inspect Objects: The instrument provides a list of objects that have been allocated and deallocated. You can inspect these objects and their associated memory usage.

    5. Call Tree and Source Code: To pinpoint memory issues, use the Call Tree to identify the functions or methods responsible for memory allocation. You can then inspect the associated source code in the Source View.

    Detecting Memory Leaks with the Leaks Instrument

    Retain Cycle

    A retain cycle in Swift occurs when two or more objects hold strong references to each other in a way that prevents them from being deallocated, causing a memory leak. This situation is also known as a “strong reference cycle.” It’s essential to understand retain cycles because they can lead to increased memory usage and potential app crashes.  

    A common scenario for retain cycles is when two objects reference each other, both using strong references. 

    Here’s an example to illustrate a retain cycle:

    class Person {
        var name: String
        var pet: Pet?
    
        init(name: String) {
            self.name = name
        }
    
        deinit {
            print("(name) has been deallocated")
        }
    }
    
    class Pet {
        var name: String
        var owner: Person?
    
        init(name: String) {
            self.name = name
        }
    
        deinit {
            print("(name) has been deallocated")
        }
    }
    
    var rohit: Person? = Person(name: "Rohit")
    var jerry: Pet? = Pet(name: "Jerry")
    
    rohit?.pet = jerry
    jerry?.owner = rohit
    
    rohit = nil
    jerry = nil

    In this example, we have two classes, Person and Pet, representing a person and their pet. Both classes have a property to store a reference to the other class (person.pet and pet.owner).  

    The “Leaks” instrument is designed to detect memory leaks in your app. 

    Here’s how to use it:

    1. Launch Instruments in Xcode: First, open your project in Xcode.  

    2. Commence Profiling: To commence the profiling process, navigate to the “Product” menu and select “Profile.”  

    3. Select the Leaks Instrument: Within the Instruments interface, choose the “Leaks” instrument from the available options.  

    4. Trigger the Memory Leak Scenario: To trigger the scenario where memory is leaked, interact with your application. This interaction, such as creating a retain cycle, will induce the memory leak.

    5. Identify Leaked Objects: The Leaks Instrument will automatically detect and pinpoint the leaked objects, offering information about their origins, including backtraces and the responsible callers.  

    6. Analyze Backtraces and Responsible Callers: To gain insights into the context in which the memory leak occurred, you can inspect the source code in the Source View provided by Instruments.  

    7. Address the Leaks: Armed with this information, you can proceed to fix the memory leaks by making the necessary adjustments in your code to ensure memory is released correctly, preventing future occurrences of memory leaks.

    You should see memory leaks like below in the Instruments.

    The issue in the above code is that both Person and Pet are holding strong references to each other. When you create a Person and a Pet and set their respective references, a retain cycle is established. Even when you set rohit and jerry to nil, the objects are not deallocated, and the deinit methods are not called. This is a memory leak caused by the retain cycle. 

    To break the retain cycle and prevent this memory leak, you can use weak or unowned references. In this case, you can make the owner property in Pet a weak reference because a pet should not own its owner:

    class Pet {
        var name: String
        weak var owner: Person?
    
        init(name: String) {
            self.name = name
        }
    
        deinit {
            print("(name) has been deallocated")
        }
    }

    By making owner a weak reference, the retain cycle is broken, and when you set rohit and jerry to nil, the objects will be deallocated, and the deinit methods will be called. This ensures proper memory management and avoids memory leaks.

    Best Practices for Memory Optimization

    In addition to using Instruments, consider the following best practices for memory optimization:

    1. Release Memory Properly: Ensure that memory is released when objects are no longer needed.

    2. Use Weak References: Use weak references when appropriate to prevent strong reference cycles.

    3. Using Unowned to break retain cycle: An unowned reference does not increment or decrease an object’s reference count. 

    3. Minimize Singletons and Global Variables: These can lead to retained objects. Use them judiciously.

    4. Implement Lazy Loading: Load resources lazily to reduce initial memory usage.

    Conclusion

    Optimizing memory usage is an essential part of creating high-quality iOS apps. 

    Instruments, integrated into Xcode, is a versatile tool that provides insights into memory allocation, leaks, and CPU-intensive code. By mastering these tools and best practices, you can ensure your app is memory-efficient, stable, and provides a superior user experience. Happy profiling!