How can I access the pins on an MKMapView via XCTest's UI Tests?
I want to count the number, verify specific ones are there (based on accessibility id or title), etc.
There doesn't seem to be a XCUIElementType for MKAnnotation.
I'm having a hard time finding any documentation on MKMapView + XCTest.
The annotation views aren't under maps unfortunately. You will find map points of interest there instead. To query for an annotation view you should use XCUIApplication().windows.element.otherElements["Custom Identifier"].
Add the accessibilityIdentifier = "Custom Identifier" to your annotation view, not annotation. MKAnnotation doesn't implement accessibilityIdentifier.
Use UIAccessibilityIdentification.
class Annotation: NSObject, MKAnnotation, UIAccessibilityIdentification {
let coordinate: CLLocationCoordinate2D
let title: String?
var accessibilityIdentifier: String?
init(title: String?, coordinate: CLLocationCoordinate2D) {
self.title = title
self.coordinate = coordinate
}
}
let coordinate = CLLocationCoordinate2DMake(40.2853, -73.3382)
let annotation = Annotation(title: "A Place Title", coordinate: coordinate)
annotation.accessibilityIdentifier = "Some Identifier"
let mapView = MKMapView()
mapView.addAnnotation(annotation)
Under test you can reference the annotation via otherElements.
let app = XCUIApplication()
let annotation = app.maps.element.otherElements["Custom Identifier"]
annotation.tap()
In my map i had only 1 pin and i could access to it with Map pin identifier like this:
let annotation = app.otherElements.matching(identifier: "Map pin").firstMatch
XCTAssertTrue(annotation.exists)
annotation.tap()
Related
The default MapMarker hides information that is under it, for example a city name. If zoomed closed enough the marker doesn't cover the name anymore and the name is displayed. When using a custom annotation view both the city name and the annotation is displayed, it looks a lil messy. Is it possible to get the behaviour from MapMarker when using custom annotations?
MapMarker: https://i.stack.imgur.com/OhQxj.png
Custom: https://i.stack.imgur.com/GwO23.png
Maybe something with clustering or collisions, but seems do be annotations colliding with annotations.
setting the value of .displayPriority to some value seems to do the trick. Don't know which value to set though.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let annotation = MKAnnotationView(annotation: annotation, reuseIdentifier: "annotation")
annotation.image = UIImage(systemName: "circle.fill")
annotation.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
annotation.displayPriority = .defaultHigh
return annotation
}
I have the following SwiftUI View:
struct ProductView: View {
#ObservedObject var productViewModel: ProductViewModel
var body: some View {
VStack {
ZStack(alignment: .top) {
if(self.productViewModel.product != nil) {
URLImage(url: self.productViewModel.product!.imageurl, itemColor: self.productViewModel.selectedColor)
}
else {
Image("loading")
}
}
}
}
that observes a ProductViewModel
class ProductViewModel: ObservableObject {
#Published var selectedColor: UIColor = .white
#Published var product: Product?
private var cancellable: AnyCancellable!
init(productFuture: Future<Product, Never>) {
self.cancellable = productFuture.sink(receiveCompletion: { comp in
print(comp)
}, receiveValue: { product in
self.product = product
print(self.product) // this prints the expected product. The network call works just fine
})
}
The Product is a Swift struct that contains several string properties:
struct Product {
let id: String
let imageurl: String
let price: String
}
It is fetched from a remote API. The service that does the fetching returns a Combine future and passes it to the view model like so:
let productFuture = retrieveProduct(productID: "1")
let productVM = ProductViewModel(productFuture: productFuture)
let productView = ProductView(productViewModel: productViewModel)
func retrieveProduct(productID: String) -> Future<Product, Never>{
let future = Future<Product, Never> { promise in
// networking logic that fetches the remote product, once it finishes the success callback is invoked
promise(.success(product))
}
return future
}
For the sake of brevity, I've excluded the networking and error handling logic since it is irrelevant for the case at hand. To reproduce this as quickly as possible, just initialize a mock product with some dummy values and pass it to the success callback with a delay like this:
let mockproduct = Product(id: "1", imageurl: "https://exampleurl.com", price: "$10")
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: {
promise(.success(mockproduct))
})
Once the product arrives over the network, it is assigned to the published product property.
The fetching works and the correct value is assigned to the published property. Obviously this happens after the view has been created since the network call takes some time. However, the View never updates even though the published object is changed.
When I pass the product directly through the View Model initializer rather than the future, it works as expected and the view displays the correct product.
Any suggestions on why the view does not react to changes in the state of the view model when it is updated asynchronously through the combine future?
EDIT: When I asked this question I had the ProductViewModel + ProductView nested inside another view. So basically the productview was only a part of a larger CategoryView. The CategoryViewmodel initialized both the ProductViewModel and the ProductView in a dedicated method:
func createProductView() -> AnyView {
let productVM = productViewModels[productIndex]
return AnyView(ProductView(productViewModel: productVM))
}
which was then called by the CategoryView on every update. I guess this got the Published variables in the nested ProductViewModel to not update correctly because the view hierarchy from CategoryView downwards got rebuilt on every update. Accordingly, the method createProductView got invoked on every new update, resulting in a completely new initialization of the ProductView + ProductViewModel.
Maybe someone with more experience with SwiftUI can comment on this.
Is it generally a bad idea to have nested observable objects in nested views or is there a way to make this work that is not an antipattern?
If not, how do you usually solve this problem when you have nested views that each have their own states?
I have been iterating on patterns like this to find what I think works best. Not sure what the problem is exactly. My intuition suggests that SwiftUI is having trouble making updates on the != nil part.
Here is the pattern that I have been using which has been working.
Define an enum for states in your networking logic
public enum NetworkingModelViewState {
case loading
case hasData
case noResults
case error
}
Add the enumeration as a variable on your "View Model"
class ProductViewModel: ObservableObject {
#Published public var state: NetworkingModelViewState = .loading
}
Update the state as you progress through your networking
self.cancellable = productFuture.sink(receiveCompletion: { comp in
print(comp)
}, receiveValue: { product in
self.product = product
self.state = NetworkingModelViewState.hasData
print(self.product) // this prints the expected product. The network call works just fine
})
Now make a decision in your SwiftUI based on the Enum value
if(self.productViewModel.state == NetworkingModelViewState.hasData) {
URLImage(url: self.productViewModel.product!.imageurl, itemColor: self.productViewModel.selectedColor)
}
else {
Image("loading")
}
Musings ~ It's hard to debug declarative frameworks. They are powerful and we should keep learning them but be aware of getting stuck. Moving too SwiftUI has forced me to really think about MVVM. My takeaway is that you really need to separate out every possible variable that controls your UI. You should not rely on checks outside of reading a variable. The Combine future pattern has a memory leak that Apple will fix next release. Also, you will be able to switch inside SwiftUI next release.
I have a SwiftUI view that has a Form that contains a DatePicker:
struct GettingUpTimeSettingView: View {
#State private var viewModel = GettingUpTimeSettingViewModel()
var body: some View {
VStack {
Form {
Spacer()
Text(viewModel.questionString)
.accessibility(identifier: "Question")
Spacer()
DatePicker("Alarm Time",
selection: $viewModel.gettingUpTime,
displayedComponents: .hourAndMinute)
.accessibility(identifier: "Time")
.animation(.easeInOut)
Spacer()
Text(viewModel.explanationString)
.accessibility(identifier: "Explanation")
}
}
}
}
And an XCTestCase class for UI Testing:
class SleepyGPIntroUITests: XCTestCase {
private var app: XCUIApplication!
override func setUp() {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
greeting = app.staticTexts["Greeting"]
}
override func tearDown() {
app = nil
}
func test_InitialScreen_ChangesTo_GettingUpScreen_Automatically() {
//Given
let questionText = "What time do you want to get up?"
let explanationText = "This should be the same time every day, including at weekends and on days off. Our best chance of great sleep comes when we have a regular routine."
let timeText = "7:00am"
let question = app.staticTexts["Question"]
let explanation = app.staticTexts["Explanation"]
let time = app.staticTexts["Time"]
//When
_ = time.waitForExistence(timeout: 2)
//Then
XCTAssertFalse(greeting.exists)
XCTAssertEqual(question.label, questionText, "Should show question at top of view")
XCTAssertEqual(explanation.label, explanationText, "Should show explanation in view")
XCTAssertEqual(time.label, timeText, "Should show the correct default time")
}
When I run the test, it fails and gives me this message:
Failed to get matching snapshot: No matches found for Elements matching predicate '"Time" IN identifiers' from input {(
StaticText, identifier: 'Question', label: 'What time do you want to get up?',
StaticText, identifier: 'Explanation', label: 'This should be the same time every day, including at weekends and on days off. Our best chance of great sleep comes when we have a regular routine.'
)}
I'm not sure whether this is down to the DatePicker() being contained in a Form, or whether it's because I'm not using the correct XCUIElementQuery to find it?
When I move the DatePicker outside the Form, I can find it, but only by using its label, and not its accessibility identifier.
The accessibility identifiers are working fine for the Text objects.
I can also find it using
app.datePickers.firstMatch
when it's outside the form, but not when it's contained within it.
I've found this answer that describes some odd behaviour when SwiftUI objects are contained in forms, but still haven't managed to solve my problem.
Many thanks.
tl:dr Can't get XCUIElementQuery to return DatePicker element when UITesting if the DatePicker is contained in a SwiftUI Form.
I've found the answer, for anyone that's interested.
First of all, a piece of basic advice, if you're struggling to find out how to access an element when UI testing in XCode, just use the record function and access the element manually.
That's what I did, and it showed me that before a DatePicker() is tapped on in SwiftUI, it actually shows as a button, so to access it in the example above I used this code:
let alarmTimeButton = app.tables.buttons["Time"]
I'm looking at an example of using SwiftUI with Combine: MVVM with Combine Tutorial for iOS at raywenderlich.com. A ViewModel implementation is given like this:
class WeeklyWeatherViewModel: ObservableObject, Identifiable {
// 2
#Published var city: String = ""
// 3
#Published var dataSource: [DailyWeatherRowViewModel] = []
private let weatherFetcher: WeatherFetchable
// 4
private var disposables = Set<AnyCancellable>()
init(weatherFetcher: WeatherFetchable) {
self.weatherFetcher = weatherFetcher
}
}
So, this makes some sense to me. In a view observing the model, an instance of the ViewModel is declared as an ObservedObject like this:
#ObservedObject var viewModel: WeeklyWeatherViewModel
And then it's possible to make use of the #Published properties in the model in the body definition of the View like this:
TextField("e.g. Cupertino", text: $viewModel.city)
In WeeklyWeatherViewModel Combine is used to take the city text, make a request on it, and turn this in to [DailyWeatherRowViewModel]. Up to here, everything is rosey and makes sense.
Where I become confused is that quite a lot of code is then used to:
Trigger a fetch when city is changed.
Keep hold of the AnyCancellable that's looking up weather data.
Copy the output of the weather look up in to dataSource by a sink on the weather fetch Publisher`
It looks like this:
// More in WeeklyWeatherViewModel
init(
weatherFetcher: WeatherFetchable,
scheduler: DispatchQueue = DispatchQueue(label: "WeatherViewModel")
) {
self.weatherFetcher = weatherFetcher
_ = $city
.dropFirst(1)
.debounce(for: .seconds(0.5), scheduler: scheduler)
.sink(receiveValue: fetchWeather(forCity:))
}
func fetchWeather(forCity city: String) {
weatherFetcher.weeklyWeatherForecast(forCity: city)
.map { response in
response.list.map(DailyWeatherRowViewModel.init)
}
.map(Array.removeDuplicates)
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] value in
guard let self = self else { return }
switch value {
case .failure:
self.dataSource = []
case .finished:
break
}
},
receiveValue: { [weak self] forecast in
guard let self = self else { return }
self.dataSource = forecast
})
.store(in: &disposables)
}
If I look in Combine for the definition of the #Published propertyWrapper, it seems like all does is provide projectedValue which is a Publisher, which makes it seem like it ought to be possible for WeeklyWeatherViewModel to simply provide the Publisher fetching weather data and for the view to make use of this directly. I don't see why the copying in to a dataSource is necessary.
Basically, what I'm expecting is there to be a way for SwiftUI to directly make use of a Publisher, and for me to be able to put that publisher externally from a View implementation so that I can inject it. But I've no idea what it is.
If this doesn't seem to make any sense, that figures, as I'm confused. Please let me know and I'll see if I can refine my explanation. Thanks!
I don't have a definitive answer to this and I didn't find a magic way to have SwiftUI make use of a Publisher directly – it's entirely possible that there is one that eludes me!
I have found a reasonably compact and flexible approach to achieving the desired result, though. It cut down the use of sink to a single occurrence that attaches to the input (#Published city in the original code), which substantially simplifies the cancelation work.
Here's a fairly generic model that has an #Published input attribute and a #Published output attribute (for which setting is private). It takes a transform as input, and this is used to transform the input publisher, and is then sinked in to the output publisher. The Cancelable of the sink is stored.
final class ObservablePublisher<Input, Output>: ObservableObject, Identifiable {
init(
initialInput: Input,
initialOutput: Output,
publisherTransform: #escaping (AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never>)
{
input = initialInput
output = initialOutput
sinkCancelable =
publisherTransform($input.eraseToAnyPublisher())
.receive(on: DispatchQueue.main)
.sink(receiveValue: { self.output = $0 })
}
#Published var input: Input
#Published private(set) var output: Output
private var sinkCancelable: AnyCancellable? = nil
}
If you wanted a substantially less generic kind of model, you can see it's pretty easy to set up having the input (which is a publisher) be filtered in to the output.
In a view, you might declare an instance of the model and use it like this:
struct SimpleView: View {
#ObservedObject var model: ObservablePublisher<String, String>
var body: some View {
List {
Section {
// Here's the input to the model taken froma text field.
TextField("Give me some input", text: $model.input)
}
Section {
// Here's the models output which the model is getting from a passed Publisher.
Text(model.output)
}
}
.listStyle(GroupedListStyle())
}
}
And here's some silly setup of the view and its model taken from a "SceneDelegate.swift". The model just delays whatever is typed in for a bit.
let model = ObservablePublisher(initialInput: "Moo moo", initialOutput: []) { textPublisher in
return textPublisher
.delay(for: 1, scheduler: DispatchQueue.global())
.eraseToAnyPublisher()
}
let rootView = NavigationView {
AlbumSearchView(model: model)
}
I made the model generic on the Input and Output. I don't know if this will actually be useful in practice, but it seems like it might be.
I'm really new to this, and there might be some terrible flaws in this such as inefficiencies, memory leaks or retain cycles, race conditions, etc. I've not found them yet, though.
You can use URLSessionDataTaskPublisher and refactor out networking from all view models.
If you feel some part of the tutorial seems redundant, that is because it is.
MVVM in such usage is redundant and does not do the job better.
I have a refactored version (networking refactored, all view models removed) of that tutorial if you are interested in details.
Playing around with examples out there. Found a project that had a class that was a bindableobject and it didn't give any errors. Now that Xcode 11 beta 4 is out, I'm getting the error:
Type 'UserSettings' does not conform to protocol 'BindableObject'
It has a fix button on the error which when you click on that, it adds
typealias PublisherType = <#type#>
It expects you to fill in the type.
What would the type be?
class UserSettings: BindableObject {
let didChange = PassthroughSubject<Void, Never>()
var score: Int = 0 {
didSet {
didChange.send()
}
}
}
Beta 4 Release notes say:
The BindableObject protocol’s requirement is now willChange instead of
didChange, and should now be sent before the object changes rather
than after it changes. This change allows for improved coalescing of
change notifications. (51580731)
You need to change your code to:
class UserSettings: BindableObject {
let willChange = PassthroughSubject<Void, Never>()
var score: Int = 0 {
willSet {
willChange.send()
}
}
}
In Beta 5 they change it again. This time they deprecated BindableObject all together!
BindableObject is replaced by the ObservableObject protocol from the
Combine framework. (50800624)
You can manually conform to ObservableObject by defining an
objectWillChange publisher that emits before the object changes.
However, by default, ObservableObject automatically synthesizes
objectWillChange and emits before any #Published properties change.
#ObjectBinding is replaced by #ObservedObject.
class UserSettings: ObservableObject {
#Published var score: Int = 0
}
struct MyView: View {
#ObservedObject var settings: UserSettings
}
in Xcode 11.X, I verify is fine in Xcode 11.2.1, 11.3.
BindableObject is changed to ObservableObject.
ObjectBinding is now ObservedObject.
didChange should be changed to objectWillChange.
List(dataSource.pictures, id: .self) { }
You can also now get rid of the did/willChange publisher and the .send code and just make pictures #Published
The rest will be autogenerated for you.
for example:
import SwiftUI
import Combine
import Foundation
class RoomStore: ObservableObject {
#Published var rooms: [Room]
init(rooms: [Room]) {
self.rooms = rooms
}
}
struct ContentView: View {
#ObservedObject var store = RoomStore(rooms: [])
}
ref: https://www.reddit.com/r/swift/comments/cu8cqk/getting_the_errors_pictured_below_when_try_to/
SwiftUI and Combine are two new frameworks that were announced at WWDC 2019. These two frameworks received a lot of attention at WWDC 2019, as evidenced by the number of sessions in which these technologies were featured.
SwiftUI was introduced as
a revolutionary, new way to build better apps, faster.
Combine is described as
a unified declarative framework for processing values over time
Between the initial release and now (May, 2020, Swift 5.2), there have been some changes. Anyone new to SwiftUI and Combine, who may have watched the WWDC videos, may be left with a few questions as to how the two frameworks work together.
Combine defines two interfaces: Publisher and Subscriber. A publisher sends events to subscribers. See sequence diagram below.
If you start an application in SwiftUI, and then add combine, there will be no mention of a Publisher or a Subscriber, the two main players required to use Combine. Consider this very simple sample application below.
import SwiftUI
import Combine
import SwiftUI
final class ActorViewModel: ObservableObject {
var name : String
private var imageUrl : URL?
//#Published
private (set) var image : Image = Image(systemName: "photo") {
willSet {
DispatchQueue.main.async {
self.objectWillChange.send()
}
}
}
init(name: String, imageUrl: URL?) {
self.name = name
self.imageUrl = imageUrl
self.fetchImage()
}
private func fetchImage() {
guard nil != self.imageUrl,
String() != self.imageUrl!.absoluteString else { return }
let task = URLSession.shared.dataTask(with: self.imageUrl!) { (data, response, error) in
guard nil == error , nil != response, nil != data,
let uiImage = UIImage(data: data!) else { return }
self.image = Image(uiImage: uiImage)
}
task.resume()
}
}
struct ContentView: View {
#ObservedObject var actor : ActorViewModel
var body: some View {
HStack {
actor.image
.resizable()
.aspectRatio(contentMode: ContentMode.fit)
.frame(width: 60, height: 60)
Text(actor.name)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let actor = ActorViewModel(name: "Mark Hammill",
imageUrl: URL(string: "https://m.media-amazon.com/images/M/MV5BOGY2MjI5MDQtOThmMC00ZGIwLWFmYjgtYWU4MzcxOGEwMGVkXkEyXkFqcGdeQXVyMzM4MjM0Nzg#._V1_.jpg"))
return ContentView(actor: actor)
}
}
The app preview via the canvas will look like this:
The app uses a list view to display names and images of actors. There are just two classes to consider:
ContentView -- the SwiftUI View subclass
ActorViewModel -- the source of the data for the ContentView (called a ViewModel as it performs the role of VM in MVVM)
The view has a reference to the actor object, as per the class diagram below.
Although this example is using Combine, it is not immediately apparent. There is no mention of a Publisher or a Subscriber. What is going on?
Answer: Looking at the class hierarchy fills in the missing gaps. The below class diagram explains the full picture (click on the image to see it in greater detail).
Consulting Apple's documentation provides definitions for these types:
ObservedObject: A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.
ObservableObject: A type of object with a publisher that emits before the object has changed. By default an ObservableObject synthesizes an objectWillChange publisher that emits the changed value before any of its #Published properties changes.
objectWillChange: A publisher that emits before the object has changed.
PassthroughSubject: A subject that broadcasts elements to downstream subscribers. As a concrete implementation of Subject, the PassthroughSubject provides a convenient way to adapt existing imperative code to the Combine model.
First, consider what the #ObservedObject means. This is a property wrapper. A property wrapper reduces code duplication, and allows for a succinct syntax when declaring properties that hides how the property is stored and defined. In this case, the "Observed Object" is a property which observes another object.
In other words, the property is a Subscriber (from the Combine Framework). The actor is (through the use of a property wrapper) is a Subscriber, which subscribes to a Publisher, but what is the Publisher in this scenario?
The "Observable Object" is not itself the publisher, but rather has a publisher. The ActorViewModel conforms to the ObservableObject protocol. By doing so, it is provided with a publisher property called objectWillChange by an extension (which the framework provides on the ObservableObject protocol). This objectWillChange property is of type PassthroughSubject, which is a concrete type of the Publisher protocol. The passthrough subject has a property called send, which is a publisher method used to send data to any subscribers. So the property called "objectWillChange" is the Publisher.
To recap, the Subscriber is the property called actor from the ContentView class, and the Publisher is the property objectWillChange from the ActorViewModel class. What about the need for the Subscriber to Subscribe to the Publisher? The "#ObservedObject" property wrapper is itself a Subscriber, so it must subscribe to the Publisher. But how does the View find out about changes sent to the Subscriber? That is handled by the SwiftUI framework, which we never see.
Take-away: we don't need to worry about subscribing the view to the Publisher. On the other hand, we do need to worry about making sure the publisher tell the subscriber when something is about to change. When the image has been fetched from a remote server, and the data has been transformed into an image object, we call objectWillChange.send() to inform the View. Once the subscriber receives notification from the publisher that something is about to / has changed, it invalidates the view (which results in the view redrawing itself).
Summary
The way in which SwiftUI uses a ObservedObject PropertyWrapper does not on the surface give away the fact that Combine even exists in the equation. But by inspecting ObservedObject and ObservableObject, the underlying Combine framework is revealed, along with the design pattern:
subscriber --> subscribing to a publisher --> which then publishes changes --> that are received by the subscriber
References:
Blog Article
WWDC 2019 Session 204
WWDC 2019 Session 226