Implementing Apple Pay in a SwiftUI app

Introduction Sometime ago, I was working on a marketplace app, and I needed to add Apple Pay to make purchases more easily. Here are a few steps on how I did it: First Step You need to add Apple Pay capability to your project. You will need to Register a Merchant ID. I will skip this step; you can find info by following this link Setting up Apple Pay. Second Step You will need to import PassKit and create PKPaymentRequest to interact with PKPaymentAuthorizationController and PKPaymentAuthorizationControllerDelegate. func initiateApplePay() { // Create payment request let paymentRequest = PKPaymentRequest() paymentRequest.merchantIdentifier = "your_merchant_identifier" paymentRequest.countryCode = "US" paymentRequest.currencyCode = "USD" paymentRequest.supportedNetworks = [.visa, .masterCard, .amex] paymentRequest.merchantCapabilities = .threeDSecure // Add payment items from cart for item in cartItems { let paymentItem = PKPaymentSummaryItem(label: item.name, amount: item.price) paymentRequest.paymentSummaryItems.append(paymentItem) } // Add total amount let totalItem = PKPaymentSummaryItem(label: "Total", amount: totalAmount) paymentRequest.paymentSummaryItems.append(totalItem) // Present Apple Pay sheet let paymentController = PKPaymentAuthorizationController(paymentRequest: paymentRequest) paymentController.delegate = self paymentController.present(completion: nil) } Third Step Add UI and connect it with the view model. ...

May 11, 2024 · 3 min · Dmytro Chumakov

Integration testing for SwiftUI views

Introduction I have been looking for information about implementation details of integration testing. I found a lot of information, but it was theoretical and all information looked the same. I did not find a meaningful example, so I tried to come up with my own definition and sample. Integration testing means testing the behavior between modules or views to ensure they work as expected after user actions. There are two ways of conducting integration testing: The first is by unit tests, where you try to test the flowing data between view models. The second is UI tests, where you try to test if the UI items exist and navigation works correctly. I will focus on testing the flowing data between view models. ...

May 10, 2024 · 2 min · Dmytro Chumakov

UI testing SwiftUI views using XCTest Framework

Introduction After spending some time developing my personal iOS app, I found myself in a position where I needed to add UI tests to my application. The reason behind this decision was the necessity to change the architecture to make it more scalable. However, this task proved to be challenging due to certain parts of the code being tightly coupled. The situation was quite frustrating. To address this problem, I decided to incorporate UI tests that could help identify issues during the refactoring process. ...

May 8, 2024 · 2 min · Dmytro Chumakov

Building Dynamic Island for Video Streaming App

Introduction I was curious about how to add Dynamic Island and implement it into a Video Streaming App. Here are a few steps on how you can achieve this. Caveats Debugging Dynamic Island can be a bit tricky; it only works when the main app is running. If you try to run it separately, you will encounter the error SendProcessControlEvent:toPid: encountered an error: Error Domain=com.apple.dt.deviceprocesscontrolservice Code=8 "Failed to show Widget". The solution is to configure live activities and run them through the main app. Be aware that when you add a widget to the project, in some cases, it adds all main target files to Compile Sources. Implementation Dynamic Islands are divided into different sizes: minimal, compactTrailing, compactLeading, and expanded. Before proceeding, you need to add LiveActivityManager to be able to display Dynamic Islands. ...

May 6, 2024 · 2 min · Dmytro Chumakov

Building Video Streaming Widget for iOS App

Introduction I was exploring the idea of creating a YouTube-like widget for the lock screen on iOS devices. It wasn’t easy because most articles on the Internet discussed general implementations, such as for a coffee shop or a to-do list. Even when I found some similar versions, the project wouldn’t compile. I made the decision to approach it my way, so here’s what I found out: Caveats After being stuck for two or more hours without understanding why, after tapping on a button, I wasn't able to receive a callback from it and the widget always opened the main iOS app, I realized that I forgot to add AppIntent - without it, you can’t handle actions for iOS 17. import AppIntents struct ButtonIntent: AppIntent { static let title: LocalizedStringResource = "ButtonIntent" @Parameter(title: "id") var id: String func perform() async throws -> some IntentResult { if id == Command.playPause.rawValue { DataModel.shared.isPlaying.toggle() } return .result() } } Another crucial point is not to forget to add an explicit init. If you don’t implement it explicitly, it will not work. import AppIntents struct ButtonIntent: AppIntent { static let title: LocalizedStringResource = "ButtonIntent" @Parameter(title: "id") var id: String init(id: String) { self.id = id } init() {} func perform() async throws -> some IntentResult { if id == Command.playPause.rawValue { DataModel.shared.isPlaying.toggle() } return .result() } } Lastly, I attempted to add a Slider, but I found that it’s not supported by the widget. My solution was to choose a ProgressView instead. Implementation struct YouTubeLockScreenWidget: View { var body: some View { VStack { Spacer() ProgressView(value: DataModel.shared.currentTime, total: DataModel.shared.totalTime) .progressViewStyle(.linear) Spacer() HStack { ForEach(Command.allCases) { command in Button(intent: ButtonIntent(id: command.id)) { Image(systemName: imageSystemName(isPlaying: DataModel.shared.isPlaying, command: command)) } } } } } } final class DataModel { static let shared = DataModel() var isPlaying: Bool = false var currentTime: TimeInterval = 34 var totalTime: TimeInterval = 304 } enum Command: String, CaseIterable { case previous case playPause case next } extension Command: Identifiable { var id: String { rawValue } } func imageSystemName(isPlaying: Bool, command: Command) -> String { switch command { case .playPause: if isPlaying { return "pause.fill" } else { return "play.fill" } case .next: return "forward.fill" case .previous: return "backward.fill" } } I had not replaced default generated code when I was adding widget to the project. I just added YouTubeLockScreenWidget to generated VideoStreamingWidgetEntryView. ...

May 5, 2024 · 2 min · Dmytro Chumakov