Kendall Gelner is a Senior iOS Engineer at Fieldwire. Previous, he was the founding iOS Architect at Nami ML. He is well regarded in the iOS development community for his technical knowledge and platform experience going back to the App Store launch. The last SDK Kendall was responsible for shipped inside of some of the most widely installed apps, reaching more than 200 million devices.
Learn the difference between an iOS Unit Test and UI Test and when to use each. Discover how to move a Unit Test into a UI Test plus some helpful tips to deal how unit tests and UI Tests are run.
In this article, we'll talk about the differences between an iOS Unit Test and UI Test. We’ll also cover when you might want to choose one type of test over the other.
We'll also give some advice on how to move a iOS Unit Test into a UI Test. Also, we’ll look at when each is appropriate to use. Finally, some helpful tips for Unit Test and UI Test runs.
For those new we'll define what are the fundamentals of these two different kinds of tests. In both an iOS Unit Test and UI Test the test class inherits from XCTestCase. Then, the tests run in a simulator instance. So, what is the difference between the two kinds of tests?
A UI Test focused on manipulating UI elements. This is so you can test that UI interactions cause the correct behavior. UI Tests can also inspect values of UI elements to see if an app runs as expected.
You can manually create UI Tests. Also, you can create them by recording a series of .UI actions. This sequence is repeatable via a test. You can also add further checks within the test to verify application state.
However, UI Tests can only manipulate and inspect UI elements. The test classes themselves are running in a separate process from your application. They have no access to the internal app state. They can only inspect what appears in the UI (via accessibility features of iOS).
UI Tests also have an important aspect. Between tests, the application launches separately. Also, you are able to suspend and resume an application in a UI Test. This is because the test has access to an app instance to manipulate.
A Unit Test is a test that runs within the same process as your app. This means it is free to call internal methods of the app or SDK you are testing (via an @testable import statement). Thus, you can inspect state while running, or activate specific methods for testing.
However, an iOS Unit Test cannot access anything related to the UI on screen. A Unit Test also cannot access the running XCUIApplication instance in order to do things like suspend or re-launch the application.
Also, each testing run spawns one testing application instance used for all Unit Tests. Importantly, Unit Tests run sequentially when housed in separate files. Unit Tests may run in parallel when housed in a single file.
This diagram summarizes the difference between the two kinds of tests:
👉Read more: Apple SDK: Smaller Size & Improved ML Debugging
The choice to make aUI Test or Unit Test depends on what you are trying to test. If you want to verify screen content or interact with UI you should use a UI Test. If you want to inspect application or SDK state or call internal methods, you will generally start with a Unit Test.
However you may come to a point where you run into the limitation of either type of test.
In the middle of a UI Test you might want to verify some internal application state.
For an iOS Unit Test, other running tests can alter internal state. This can cause tests to fail that might not otherwise when run by itself.
In these cases, try to start up the application in a clean state where no other tests have run, even if other tests have run previously.
Sometimes that means you want to be able to re-launch the application in a certain way while still manipulating state inside. So how can we work with the differences between the two test types?
Given we may want to use features of both UI and Unit testing in some tests, what can we do?
A flexible approach involves combining both kinds of tests. Use a UI Test that accesses screen elements that trigger internal methods in your application. Then, populates text fields with results or state of your application/SDK.
As these UI elements exist purely for testing, you can have a hidden testing screen within an app that a UI Test can trigger. Alternatively, you can create a separate testing application for something like an internal SDK that holds UI elements just for testing.
The general approach to building a test that takes this approach is:
Let's see those steps in action within a real test:
The test relies on getting access to specific UI elements. A button to activate a purchase and a label where test results are stored.
The button itself is obtained by using the text from the button. For the label, we have to add a bit of additional code in the main application view controller that displays the purchase button and results label:
The XCUITest cases use Accessibility to get at ID values to recognize the label. An accessibility value is set to the results of the test so set it (and the label text) to an initial value that indicates the test has not yet completed.
The code for the button press is kept simple to do just one task. It could be more complex:
👉Read more: Test In-App Purchases iOS Guide
You may need to clear out application state to run a particular test. There are a few options:
For an iOS Unit Test, you can implement a method accessible via the @testable import that class out pre-existing state that may be set from other tests within the application.
For UI Tests, you can create a command line parameter that you can pass to a test on launch. This would be used clear out state from previous launches. It would also access a method similar to the one described for cleaning application state.
At times however, you may need to be sure a iOS Unit Test is run after a clean launch within other tests run.
In that case, you can move a Unit Test into a UI Test class. Putting the code you wish to test into the application, triggering it via a UI element, and putting the results into a label for the test case to verify as described previously.
For a fully clean start you can pass in a launch argument from your test case:
Note that app.launch() re-launches the application after the initial test being activated runs it.
The code in the application to look for arguments being passed and act on them looks like:
Testing is always a balance between deciding what things are important to test for, and what is possible with your tools. Techniques like these should expand what is possible to test. Plus, gain greater flexibility in testing your app to the fullest extent possible.
if(window.strchfSettings === undefined) window.strchfSettings = {};
window.strchfSettings.stats = {url: "https://nami.storychief.io/en/maximize-ios-testing-coverage-unit-test-ui-test?id=373311933&type=26",title: "Maximize iOS Testing Coverage: Unit Test vs. UI Test",id: "51b60849-ff21-4408-b48f-9543da3cae59"};
(function(d, s, id) {
var js, sjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) {window.strchf.update(); return;}
js = d.createElement(s); js.id = id;
js.src = "https://d37oebn0w9ir6a.cloudfront.net/scripts/v0/strchf.js";
js.async = true;
sjs.parentNode.insertBefore(js, sjs);
}(document, 'script', 'storychief-jssdk'))
Incorporating in-app purchases (IAPs) is a crucial step for monetizing your iOS app. With the introduction of StoreKit 2, Apple has simplified the process, offering powerful new tools and a more straightforward API. This guide will walk you through the steps to add in-app purchases to your SwiftUI app using StoreKit 2, ensuring a seamless experience for both developers and users.
SwiftUI is a modern framework introduced by Apple for building user interfaces across all Apple platforms using a declarative Swift syntax. Launched in 2019, SwiftUI allows developers to create robust, dynamic, and interactive user interfaces with less code and more efficiency. Instead of manually managing the state of the UI, SwiftUI handles it automatically, allowing developers to focus on defining what the UI should look like for any given state.
SwiftUI works seamlessly with other Apple frameworks and is designed to be integrated with existing UIKit and AppKit projects.
First, import the StoreKit framework to access the necessary classes and methods for implementing in-app purchases:
import StoreKit
StoreKit is the framework provided by Apple to handle in-app purchases and subscriptions. By importing StoreKit, you gain access to various tools and functions that simplify the process of fetching products, managing transactions, and verifying purchases. StoreKit 2, introduced in iOS 15, enhances this functionality by offering a more modern, Swift-friendly API that leverages Swift concurrency.
Create a class to manage in-app purchase products using StoreKit 2:
class IAPManager: ObservableObject {
@Published var products: [Product] = []
init() {
Task {
await self.retrieveProducts()
}
}
func retrieveProducts() async {
do {
let productIDs = ["com.yourapp.productid1", "com.yourapp.productid2"]
products = try await Product.products(for: productIDs)
} catch {
print("Failed to fetch products: \(error)")
}
}
}
This IAPManager
class handles the fetching of available products from the App Store. The @Published
property wrapper allows SwiftUI views to reactively update whenever the products
array changes.
Add methods for purchasing and handling transactions:
extension IAPManager { func purchase(_ product: Product) async -> Bool {
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try self.verifyPurchase(verification)
await transaction.finish()
return true
case .userCancelled, .pending:
return false
@unknown default:
return false
}
} catch {
print("Purchase failed: \(error)")
return false
}
}
private func verifyPurchase(_ verification: VerificationResult<Transaction>) throws -> Transaction {
switch verification {
case .unverified:
throw NSError(domain: "Verification failed", code: 1, userInfo: nil)
case .verified(let transaction):
return transaction
}
}
}
The purchase
method handles the entire purchasing process, including starting the purchase, verifying the transaction, and finishing it. The verifyPurchase
method ensures the transaction is valid and comes from the App Store.
Use the IAPManager in your SwiftUI views:
struct ContentView: View {
@StateObject var iapManager = IAPManager()
var body: some View {
List(iapManager.products, id: \.id) { product in
VStack(alignment: .leading) {
Text(product.displayName)
Text(product.description)
Text(product.displayPrice)
Button("Buy") {
Task {
await iapManager.purchase(product)
}
}
}
}
}
}
This SwiftUI view displays a list of available products, with buttons to initiate the purchase process. The @StateObject
property wrapper ensures that the IAPManager
instance is created and retained correctly within the view.
To manage subscriptions, you need to handle the different states a subscription can be in, such as active, expired, or in a billing retry period.
func handleSubscriptions() async {
do {
let statuses = try await Product.subscriptions.allStatuses()
for status in statuses {
switch status.state {
case .subscribed:
// User is subscribed
break
case .expired:
// Subscription expired
break
case .inBillingRetryPeriod:
// Billing issue
break
default:
break
}
}
} catch {
print("Failed to fetch subscription statuses: \(error)")
}
}
Users expect to be able to restore purchases, especially if they switch devices. Implement a restore function to handle this.
func restorePurchases() async {
do {
try await AppStore.sync()
try await updateUserPurchases()
} catch {
print("Failed to restore purchases: \(error)")
}
}
Listening for transaction updates ensures you catch any purchases made on other devices or pending purchases.
@MainActor
func listenForTransactions() {
Task {
for await verificationResult in Transaction.updates {
do {
let transaction = try self.verifyPurchase(verificationResult)
// Handle the transaction
await transaction.finish()
} catch {
print("Transaction verification failed: \(error)")
}
}
}
}
Keep track of the user's purchased items to unlock content appropriately.
@MainActorfunc updateUserPurchases() async {
do {
for await verificationResult in Transaction.currentEntitlements {
let transaction = try self.verifyPurchase(verificationResult)
switch transaction.productType {
case .consumable:
// Handle consumable purchase
break
case .nonConsumable:
// Handle non-consumable purchase
break
case .autoRenewable:
// Handle subscription
break
default:
break
}
}
} catch {
print("Failed to update user purchases: \(error)")
}
}
👉Read more: Monetizing SwiftUI Apps: IAP Subscriptions
There are different kinds of data you can hold purchase activity in, how to write code to handle purchase activity with StoreKit, or use a framework like Nami which manages StoreKit transactions for you and can work with data stores like the ones described below.
As subscriptions are a model where a customer gives you money over time in return for some continuing value of your application, it's a good idea to keep in mind the customer journey with your application - are they brand new? Might they have been using your app for some time and are about to subscribe? If they have subscribed, are they going to renew, or have they opted to cancel a current subscription and let it just run out?
Fundamentally, the simplest things you would want to track for a subscription are:
Just from those, you can adjust displays in your application to access content, or to provide some additional messaging as the end of a subscription draws near.
This is what most people think of when they talk about In-App Purchases. In this case, the purchase is made only once and lasts forever. So it's enough to simply track if something is purchased.
Consumable purchases allow a user to make the same purchase more than once. Two common use cases are coin packs in games and apps that let you buy and spend credits. For another uses case of consumable purchases, see our guide on creating a tip jar for your app.
Since users can make a purchase more than once, it's a good idea to keep track of how many times they may have purchased, for either messaging around thanking them for each purchase or adapting the application to reward multiple purchases in some way as well as the user’s credit balance:
You can add more details around any of those items, but those are great starting places.
When you decide what kind of purchase data you want to preserve and react to, you then need to decide how to store it in a way that a SwiftUI application can react to it. In order to do that, you can make use of the Combine framework, which allows you to have an object that publishes changes. For SwiftUI, that means making an ObservableObject, with Published properties that correspond to the kinds of things you'll be looking for.
An Observable object is made by declaring one or more Published properties, that when changed will notify any views using the properties.
For this example, we’ll create an ObservableObject for a subscription.
Then you can either add methods to the ObservableObject to alter the Published properties as needed, or modify them externally via some other code. An example of setting up a listener for an Observable object to change purchase state for the object above would be to add an init like so:
When you process a purchase with StoreKit, check to see that the purchase has completed, and then send a notification that triggers the ObservableObject to update values:
Note that properly validating a purchase requires looking at the receipt which should be done on a server. Take a look at this blog post to get started.
Correctly updating subscription state over time also requires a server to process Apple’s Server to Server notifications and correctly update a model of the customer’s subscription lifecycle. This is beyond the scope of this article.
If you are using the Nami ML platform, we automatically manage the subscription lifecycle for you. The code sample below provides an example of fully managing a customer’s access to a subscription in a SwiftUI app.
Once you have an ObservableObject, you'll need to be able to use it for a variety of views across your application. You also want to create that ObservableObject as early as possible, since purchase messages might be triggered as soon as your application launches.
You could simply create an instance of the object and pass it into every view via an initializer, but chances are you will not need to know about purchase status in every view. Thus, it's easier to add your ObservableObject into the application environment where any view can access it directly without having to have been passed in the object.
The best place to do setup and add the ObservableObject to the environment is in your App class, where your initial Scene is created.
Now in any class that you want to use your ObservableObject properties, you can just add the object from the environment.
👉Read more: What's New with In-App Purchases at WWDC 21
When your users make a purchase, that purchase unlocks an entitlement that grants them access to the paid features of your app.
Now that we have all the basics in place, let’s look at a few different options of how you can grant access to paid features in your app.
You can optionally display a view, based on the current state of purchases.
You can make an even more complex choice, deciding to display a view based not just on purchase state, but on some other variable like subscription expiration.
In this example, there may be a completely different view for a paid subscriber than there is for a free user.
In the same way, you can conditionally add whole views, you can also opt to change content based on the purchase state.
This can be a good way to show paid features that exist on a view or enable a disabled button that does not work for a free customer.
When you have a button that accesses paid content in your application, you can check if the user has the correct access for that content.
If they do, allow them to see the content, otherwise you can present your paywall with your purchase options.
This is a general pattern you may use to protect paid content in your app.
A full StoreKit implementation is beyond the scope of this article and requires some updates to your app code as well as some server-side components as well.
The Nami platform also takes care of the complexity of StoreKit integration so you can focus on other aspects of your app. Check out our SwiftUI page or Quickstart Guide for an overview.
SwiftUI makes it very easy to modify UI in reaction to purchases via waiting for state changes. You should think about adding support for purchases as early as possible during the design of your application, so the integration of purchase boundaries feels as natural as possible and doesn't cause you extra work rearranging UI to support purchases later.
What every iOS developer should know before submitting an app to the App Store for review.
Its time, development is complete, bugs squashed, and code is clean. Approvals are all done, and you’re now good to go on submitting your app to the Apple App Store. After working on your app for days, weeks or even months…. there is light at the end of that tunnel!
Let’s get started with a few tips on some key things you will need to know in advance:
If you haven’t already, you will need to register and handover about $99/yr into the Apple Developer Program. It’s worth it as you will get access to a slate of great benefits:
This is a key website that you will be able to gain access to upon signing up. App Store Connect is a portal used to manage everything to do with the App Store.
So now that you’ve got yourself registered, there’s still a couple of things to check before your next step.
This will be done via your App Store Connect portal. Once you are in, select the My Apps menu and then the “+” option. As you complete the information, you can click the “?” button if you have any questions about a specific field. At this stage, you would be filling out the name, description, category, minimum audience age, support URL and your privacy policy.
If you’ve created an app that involves social media and requires a login, you’d need to provide one for the Apple reviewer to access the app’s social features. Additionally, ensure your privacy policy is submitted as a live document online with a public URL.
This is also the stage where you’d be submitting your pricing. You will have a few options here including a scheduled date of delivery as well as allowing for pre-orders and all territories where it will be available.
Not too much to include here except to add that you must make time to read about submitting proper screenshots required by the App Store. This link will take you right to the screenshot specifications. This is especially critical if you are using multiple sized displays (iPad, Apple Watch, etc.)
Making screenshots can be done, for example, in a couple of ways:
If you decide to not to rely on actual screenshots, you'll need a couple of tools.
First is graphics software to help with your screenshots, you can try any of these: Pixelmator, Figma, Sketch, Adobe Photoshop. TIP: Figma is free by the way and works great!
Next, you’re going to need some device mockups.
Mockuphone is free to use. Simply select your device, upload the screenshot of choice and DL the mockup. You can choose between portrait and landscape options. Import it then into your graphics app and add any text needed.
Rotato is another one that you can use. It’s a great paid app that uses a “camera” under your control on a 3D mockup. This gives you awesome angles to work with. Again, once you’ve got your shot, upload it to your graphics app.
Angle is a paid collection of mockups. They are in a variety of angles and device frames preset for you to import into your graphics app. FYI: If you pick Figma, just know that only the Sketch files will import from Angle, not the AdobeXD files.
You can use an app store screenshot generator if you don’t want to bother using mockups in graphics software. There are a couple tools that will do it for you.
With both Davinci Apps and Shotbot, you only need to select a template, upload your screenshots and add custom text.
Most importantly, be sure to show your app in action. Please do not make the rookie mistake of including a screenshot of your login screen.
First things first, ensure you’ve got no red warnings and your app is clean from errors. Then make sure your Signing and Team info is set up in Xcode. Your app version at this point should be 1.0.0 if it’s your first time publishing this app.
Publishing is much easier now as Xcode is smarter now by enabling or creating the certificates, profiles etc. on your behalf as you go along. Be sure to create the full archive by selecting the Generic iOS Device from your list of simulators. FYI: When you bundle your app in the Archive, it can be a fairly lengthy process as the size of your app will determine how long it will take.
Once you’ve got your perfect archive set, you can choose Distribute App and expect to be prompted to choose the best method of distribution. After you’ve selected iOS App Store, a couple checks to ensure the app being uploaded isn’t broken will take place. FYI: This will also take time depending on the size of your app.
If your upload is good, you will see a successful upload notification next! Congratulations! If you need a bit more help don’t forget your resources at App Store Connect to assist.
This is where you will add the build that was exported from Xcode. App Store Connect will need a bit of time to process it. You will receive a notification once your archive has been processed and is ready for use.
Once you’ve clicked the save and submit-for-review buttons, you will need to answer a couple questions regarding compliance and advertising. The review process will take about 24 to 72hrs (on average). If approved, you will get an email immediately.
Nami’s mission is to help app publishers grow their revenue using in-app subscriptions. We want to help app developers build their businesses in a way that benefits the company and the customer. Schedule a demo today to start growing your app business.