Combine practical usage examples

Introduction When working in a large codebase with a significant number of async events, I often found myself in situations where I couldn’t combine events effectively. This resulted in optimization problems and inefficient consumption of OS resources. The codebase contained closures and async/await, so it wasn’t possible to use operators like merge or combineLatest. After discovering this limitation, I decided to add new methods using Combine. I will be demonstrating this with a simple NetworkService responsible only for executing and validating requests using Combine. Let’s dive into the implementation. ...

June 22, 2024 · 2 min · Dmytro Chumakov

Testing push notifications locally in an iOS app

Introduction I always wondered how I could automate testing the push notification process. Even when Apple introduced the possibility of dragging a configured file to the simulator to display a notification, it is still a manual process. I’ll skip testing via the terminal because I think it takes more time than using an APNS file or the RocketSim app. Before I was first introduced to the RocketSim app, I used an APNS file for testing push notifications. It worked for me and my teammates, but I knew it could be better. It looks something like this: ...

June 19, 2024 · 2 min · Dmytro Chumakov

Adding Push Notifications to an iOS App

Introduction If you start a project from scratch, you need to always create some kind of service like PushNotificationService that will be responsible for handling push notification events. In this article, I want to explore a simple implementation of PushNotificationService to be able to reuse and customize it in future projects. First Step The first step is to add the Push Notifications capability to your project. Go to your project -> Signing & Capabilities -> Tap + Capability -> Search for Push Notifications. ...

June 14, 2024 · 2 min · Dmytro Chumakov

Caching data using NSCache in iOS

Introduction I was curious about caching data using NSCache for an iOS app. So, I did some digging. Here is what I found: Quick Overview NSCache helps store data in memory. When the application gets killed, it frees memory; it’s not persisted on disk. Storing data is carried out using a key-value pair mechanism like Dictionary. You can set automatic eviction to delete objects automatically. NSCache has multi-platform support: iOS, iPadOS, watchOS, macOS, and tvOS. Caveats NSCache has Objective-C roots. It can’t use struct because it is constrained to conform to AnyObject, meaning you must use class and NSString instead of String. ...

June 5, 2024 · 3 min · Dmytro Chumakov

Accessibility iOS SwiftUI

Introduction Previously, I posted about Accessibility for UIKit. The idea behind this post is to find differences between UIKit Accessibility and SwiftUI features. Similarities: Both UIKit and SwiftUI have accessibilityLabel and accessibilityHints APIs. Differences: To use dynamic type for fonts, you need additional modifiers in SwiftUI. struct ScaledFont: ViewModifier { @Environment(\.sizeCategory) var sizeCategory var name: String var size: Double func body(content: Content) -> some View { let scaledSize = UIFontMetrics.default.scaledValue(for: size) return content.font(.custom(name, size: scaledSize)) } } extension View { func scaledFont(name: String, textSize size: Double) -> some View { return self.modifier(ScaledFont(name: name, size: size)) } } To step over elements in a list, you need to add .accessibilityElement(children: .combine) to each row in SwiftUI. struct FruitCaloriesCounter: View { var body: some View { NavigationView { List(fruits) { fruit in FruitRow(fruit: fruit) .accessibilityElement(children: .combine) } .navigationTitle("Fruits Calories Counter") .accessibilityElement(children: .contain) .navigationBarTitleDisplayMode(.inline) } } } In UIKit, you can insert and remove accessibilityTraits depending on the button state: if button.isSelected { button.accessibilityTraits.insert(.header) } else { button.accessibilityTraits.remove(.header) } In SwiftUI, you need to pass .accessibilityAddTraits(selected ? [.isSelected, .isButton] : .isButton) to one modifier. Button(action: { selected.toggle() }) { Image(systemName: selected ? "star.fill" : "star") .frame(width: 44, height: 44) .accessibilityLabel("favourite") .accessibilityHint(selected ? "removes favourite" : "makes favourite") .accessibilityAddTraits(selected ? [.isSelected, .isButton] : .isButton) } .buttonStyle(.plain) Complete Sample import SwiftUI let fruits = [ Fruit(name: "Apple", calories: 52), Fruit(name: "Banana", calories: 89), Fruit(name: "Orange", calories: 47), Fruit(name: "Pineapple", calories: 50), Fruit(name: "Strawberry", calories: 32) ] struct Fruit: Identifiable { var id: String { name } let name: String let calories: Int } struct FruitRow: View { @State private var selected = false let fruit: Fruit var body: some View { HStack(spacing: 8) { VStack(alignment: .leading, spacing: 8) { Text(fruit.name) .scaledFont(name: "Helvetica", textSize: 20) .accessibilityLabel(fruit.name) Text("\(fruit.calories) per 100g") .scaledFont(name: "Helvetica", textSize: 15) .accessibilityLabel("\(fruit.calories) calories per 100 grams") } Spacer() Button(action: { selected.toggle() }) { Image(systemName: selected ? "star.fill" : "star") .frame(width: 44, height: 44) .accessibilityLabel("favourite") .accessibilityHint(selected ? "removes favourite" : "makes favourite") .accessibilityAddTraits(selected ? [.isSelected, .isButton] : .isButton) } .buttonStyle(.plain) } } } struct FruitCaloriesCounter: View { var body: some View { NavigationView { List(fruits) { fruit in FruitRow(fruit: fruit) .accessibilityElement(children: .combine) } .navigationTitle("Fruits Calories Counter") .accessibilityElement(children: .contain) .navigationBarTitleDisplayMode(.inline) } } } struct ContentView: View { var body: some View { FruitCaloriesCounter() } } #Preview { ContentView() } struct ScaledFont: ViewModifier { @Environment(\.sizeCategory) var sizeCategory var name: String var size: Double func body(content: Content) -> some View { let scaledSize = UIFontMetrics.default.scaledValue(for: size) return content.font(.custom(name, size: scaledSize)) } } extension View { func scaledFont(name: String, textSize size: Double) -> some View { return self.modifier(ScaledFont(name: name, size: size)) } } Thank you for reading! 😊

June 2, 2024 · 3 min · Dmytro Chumakov