Cannot access SwiftUI Text in XCTestCase UI Test - swift

I am trying to add UI tests to a SwiftUI project.
I have a list, which contains views - those then contain a number of views.
I cannot seem to access the furthest most view in my UI test.
I thought I could add an accessibility identifier to each element but I cannot make my test pass still.
A very simple example;
ContentView
struct ListModel: Identifiable {
let id: String
let text: String
}
struct ContentView: View {
private var state = (0..<50).map { ListModel(id: "\($0)", text: "Row \($0)") }
var body: some View {
List(state, id: \.id) { item in
ContentViewRow(text: item.text)
.accessibility(identifier: "FEED_ITEM")
}
.accessibility(identifier: "FEED")
}
}
struct ContentViewRow: View {
let text: String
var body: some View {
Text(text)
.accessibility(identifier: "CONTENT_ROW_TEXT")
}
}
Tests
class TestingSwiftUIUITests: XCTestCase {
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
let feed = app.tables["FEED"]
XCTAssert(feed.waitForExistence(timeout: 0.5))
let row0 = feed.staticTexts["FEED_ITEM"].firstMatch
XCTAssert(row0.waitForExistence(timeout: 0.5))
let textView = row0.staticTexts["CONTENT_ROW_TEXT"].firstMatch
XCTAssert(textView.waitForExistence(timeout: 0.5)) // <-- This fails.
}
}
How can I access a view inside ContentViewRow - thank you.

identifier: "CONTENT_ROW_TEXT" seems to be overriden by identifier: "FEED_ITEM"
Perhaps you can leave just "FEED_ITEM" and check for the label text if needed.
let row0 = feed.staticTexts["FEED_ITEM"].firstMatch
XCTAssert(row0.waitForExistence(timeout: 0.5))
XCTAssert(row0.label == "Row 0")

Related

Swift UI - Closure containing control flow statement cannot be used with result builder 'CommandsBuilder'

I'm trying to check if my array contains the machine learning model output, but its giving me this weird error, and I don't understand it.
Here's my code
Image Detector class:
import SwiftUI
class ImageDetector: ObservableObject {
#Published private var detector = Detector()
var imageClass: String? {
detector.results
}
// MARK: Intent(s)
func detect(uiImage: UIImage) {
guard let ciImage = CIImage (image: uiImage) else { return }
detector.detect(ciImage: ciImage)
}}
Content View:
#ObservedObject var detector: ImageDetector
let collectionArr = ["Test", "test2", "test3", "test4"]
var body: some View {
VStack {
var mlOutput = detector.imageClass
Group{
// Outputing the information of the respective Item
if collectionArr.contains(mlOutput) {
HStack{
Text("")
.font(.caption)
Text(mlOutput)
.bold()
}
}
else {
HStack{
Text("This is not in the array")
.font(.caption)
}
}
}
Since .contains returns a Bool, you don't need to provide a binding to a variable (which is used in cases where you have a statement returning an Optional value).
//remove var mlOutput... line here
Group{
if let mlOutput = detector.imageClass, collectionArr.contains(mlOutput) {
HStack{
Text("").font(.caption)
Text(mlOutput).bold()
}
} else {
HStack {
Text("This is not in the array").font(.caption)
}
}
}
This is assuming that mlOuput (which maybe is a typo for mlOutput?) is a String

SwiftUI: Integration of dictionaries and quicklook

I'm seeking some help for my iOS app and hope someone can get me on the right path!
FYI: I'm no developper, I'm an architectural student trying to work on a project for a museum.
So, I have this view which contains multiple cards for different objects that are in the museum and when I tap on it, I go to a new view with it's image, description,... + a PlayButton (the Playbutton is a vectorial triangle with some overlay for it's frame, background..).
I have it configured in a way for me to be able to add easily more objects with it corresponding informations with a dictionary.
struct Card: Identifiable {
let id = UUID()
var index: Int
var title: String
var subtitle: String
var description: String
var text: String
var image: String
var background: String
var logo: String }
var cards = [
Card(index: 1, title: "Universale".uppercased(), subtitle: "Joe Colombo".uppercased(), description: "Short description", text: "Long description", image: "Universale", background: "Background 5", logo: "Logo 5"),]
Then I call for this variable in my main view with this code to see every cards I want to show.
var featured: some View {
ScrollView(.horizontal, showsIndicators: false){
HStack(spacing: 16) {
ForEach(cards) { cards in
CardsItem(cards: cards)
.onTapGesture {
showCourse = true
selectedCourse = cards
}
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isButton)
}
}
.padding(20)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.sheet(isPresented: $showCourse) {
CardsView(namespace: namespace, cards: $selectedCourse, isAnimated: false)
}
}
And my complete view is coded like this:
var content: some View {
ScrollView {
scrollDetection
Spacer()
.padding(.top, 40)
Text("Essayez les objets de la collection chez vous!")
.font(.body.bold())
.foregroundColor(.primary)
.sectionTitleModifier()
Group {
featured
Text("Porte-à-faux".uppercased())
.font(.body.bold())
.foregroundColor(.secondary)
.sectionTitleModifier()
featured2
.padding(.bottom, 60)
}
}
.coordinateSpace(name: "scroll")
.overlay(NavigationBar(title: "Collection", contentHasScrolled: $contentHasScrolled))
}
At this point, everything works like I want. But now I want to add a way for each object to access a usdz file and show it in AR QuickLook when I tap on the play button. I managed to get it working (Kinda, I currently need to kill the app if I want to go back afterwards) with one file that launches on every cards but I can't find a way to launch the corresponding usdz file for each object.
How would I approach this? I hope I was clear enough.
Thanks in advance!
Here's the code I used for the AR QuickLook
class ARQLViewController: UIViewController, QLPreviewControllerDataSource {
let assetName = "Pantone"
let assetType = "usdz"
let allowsContentScaling = false
let canonicalWebPageURL = URL(string: "nil")
override func viewDidAppear(_ animated: Bool) {
let previewController = QLPreviewController()
previewController.dataSource = self
present(previewController, animated: true, completion: nil)
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
guard let path = Bundle.main.path(forResource: assetName, ofType: assetType) else {
fatalError("Couldn't find the supported asset file.")
}
let url = URL(fileURLWithPath: path)
let previewItem = ARQuickLookPreviewItem(fileAt: url)
previewItem.allowsContentScaling = allowsContentScaling // default = true
previewItem.canonicalWebPageURL = canonicalWebPageURL // default = nil
return previewItem
}}

SwiftUI NavigationLink - "lazy" destination? Or how to not code duplicate `label`?

(on macOS Big Sur with Xcode 12 beta)
One thing I've been struggling with in SwiftUI is navigating from e.g. a SetUpGameView which needs to create a Game struct depending on e.g. player name inputted by the user in this view and then proceed with navigation to GameView via a NavigationLink, being button-like, which should be disabled when var game: Game? is nil. The game cannot be initialized until all necessary data has been inputted by the user, but in my example below I just required playerName: String to be non empty.
I have a decent solution, but it seems too complicated.
Below I will present multiple solutions, all of which seems too complicated, I was hoping you could help me out coming up with an even simpler solution.
Game struct
struct Game {
let playerName: String
init?(playerName: String) {
guard !playerName.isEmpty else {
return nil
}
self.playerName = playerName
}
}
SetUpGameView
The naive (non-working) implementation is this:
struct SetUpGameView: View {
// ....
var game: Game? {
Game(playerName: playerName)
}
var body: some View {
NavigationView {
// ...
NavigationLink(
destination: GameView(game: game!),
label: { Label("Start Game", systemImage: "play") }
)
.disabled(game == nil)
// ...
}
}
// ...
}
However, this does not work, because it crashes the app. It crashes the app because the expression: GameView(game: game!) as destionation in the NavigationLink initializer does not evaluate lazily, the game! evaluates early, and will be nil at first thus force unwrapping it causes a crash. This is really confusing for me... It feels just... wrong! Because it will not be used until said navigation is used, and using this particular initializer will not result in the destination being used until the NavigationLink is clicked. So we have to handle this with an if let, but now it gets a bit more complicated. I want the NavigationLink label to look the same, except for disabled/enabled rendering, for both states game nil/non nil. This causes code duplication. Or at least I have not come up with any better solution than the ones I present below. Below is two different solutions and a third improved (refactored into custom View ConditionalNavigationLink View) version of the second one...
struct SetUpGameView: View {
#State private var playerName = ""
init() {
UITableView.appearance().backgroundColor = .clear
}
var game: Game? {
Game(playerName: playerName)
}
var body: some View {
NavigationView {
VStack {
Form {
TextField("Player name", text: $playerName)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
// All these three solution work
// navToGameSolution1
// navToGameSolution2
navToGameSolution2Refactored
}
}
}
// MARK: Solutions
// N.B. this helper view is only needed by solution1 to avoid duplication in if else, but also used in solution2 for convenience. If solution2 is used this code could be extracted to only occur inline with solution2.
var startGameLabel: some View {
// Bug with new View type `Label` see: https://stackoverflow.com/questions/62556361/swiftui-label-text-and-image-vertically-misaligned
HStack {
Image(systemName: "play")
Text("Start Game")
}
}
var navToGameSolution1: some View {
Group { // N.B. this wrapping `Group` is only here, if if let was inline in the `body` it could have been removed...
if let game = game {
NavigationLink(
destination: GameView(game: game),
label: { startGameLabel }
)
} else {
startGameLabel
}
}
}
var navToGameSolution2: some View {
NavigationLink(
destination: game.map { AnyView(GameView(game: $0)) } ?? AnyView(EmptyView()),
label: { startGameLabel }
).disabled(game == nil)
}
var navToGameSolution2Refactored: some View {
NavigatableIfModelPresent(model: game) {
startGameLabel
} destination: {
GameView(game: $0)
}
}
}
NavigatableIfModelPresent
Same solution as navToGameSolution2 but refactored, so that we do not need to repeat the label, or construct multiple AnyView...
struct NavigatableIfModelPresent<Model, Label, Destination>: View where Label: View, Destination: View {
let model: Model?
let label: () -> Label
let destination: (Model) -> Destination
var body: some View {
NavigationLink(
destination: model.map { AnyView(destination($0)) } ?? AnyView(EmptyView()),
label: label
).disabled(model == nil)
}
}
Feels like I'm missing something here... I don't want to automatically navigate when game becomes non-nil and I don't want the NavigationLink to be enabled until game is non nil.
You could try use #Binding instead to keep track of your Game. This will mean that whenever you navigate to GameView, you have the latest Game as it is set through the #Binding and not through the initializer.
I have made a simplified version to show the concept:
struct ContentView: View {
#State private var playerName = ""
#State private var game: Game?
var body: some View {
NavigationView {
VStack {
Form {
TextField("Player Name", text: $playerName) {
setGame()
}
}
NavigationLink("Go to Game", destination: GameView(game: $game))
Spacer()
}
}
}
private func setGame() {
game = Game(title: "Some title", playerName: playerName)
}
}
struct Game {
let title: String
let playerName: String
}
struct GameView: View {
#Binding var game: Game!
var body: some View {
VStack {
Text(game.title)
Text(game.playerName)
}
}
}

Issue when rearranging List item in detail view using SwiftUI Navigation View and Sorted FetchRequest

I have a NavigationView with a list showing tasks from a CoreData FetchRequest. The FetchRequest is sorted ascending on Task.dueDate. The TaskDetail view basically consists of a TextField for the title and a date picker for the date. Changing the values in the detail view works. Though I get some weird behaviour every time I try to change the date value. The date gets changed but the Navigation view automatically exits the detail view and goes back to the list view. It only happens when I change the date in such a way that the list gets rearranged due to the sorting.
How do I prevent this weird behaviour described above??
//
// ContentView.swift
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(fetchRequest: Task.requestAllTasks()) var tasks: FetchedResults<Task>
var body: some View {
NavigationView {
List(tasks, id: \.id) { task in
NavigationLink(destination: TaskDetail(task: task)) {
Text("\(task.title)")
}
}.navigationBarTitle("Tasks").navigationBarItems(trailing: Button("new") {self.addTask()})
}
}
func addTask() -> Void {
let newTask = Task(context: self.moc)
newTask.id = UUID()
newTask.title = "task \(tasks.count)"
newTask.dueDate = Date()
print("created new Task")
if (self.moc.hasChanges) {
try? self.moc.save()
print("saved MOC")
}
print(self.tasks)
}
}
struct TaskDetail : View {
#ObservedObject var task: Task
var body: some View {
VStack{
TextField("name", text: $task.title)
DatePicker("dueDate", selection: $task.dueDate, displayedComponents: .date)
.labelsHidden()
}
}
}
//
// Task.swift
import Foundation
import CoreData
public class Task: NSManagedObject, Identifiable {
#NSManaged public var id: UUID?
#NSManaged public var dueDate: Date
#NSManaged public var title: String
static func requestAllTasks() -> NSFetchRequest<Task> {
let request: NSFetchRequest<Task> = Task.fetchRequest() as! NSFetchRequest<Task>
let sortDescriptor = NSSortDescriptor(key: "dueDate", ascending: true)
request.sortDescriptors = [sortDescriptor]
return request
}
}
To create a running minimal reproducible version of this...do:
Create new Xcode "Single View App" Project. Make sure to check the
CoreData checkbox.
Copy the code for ContentView above and paste/replace in ContentView.swift.
Create a new Swift file named Task. Copy the code for Task and paste in Task.swift.
Add the entities in the ProjectName.xcdatamodeld according to the image below.
Run
I am on Xcode 11.4.
Let me know if you need me to provide more information.
Any help is much appreciated! Thanks!
UPDATE 2 (iOS 14 beta 3)
The issue seems to be fixed in iOS 14 beta 3: the Detail view does no longer pop when making changes that affect the sort order.
UPDATE
It seems Apple sees this as a feature, not a bug; today they replied to my feedback (FB7651251) about this issue as follows:
We would recommend using isActive and managing the push yourself using
the selection binding if this is the behavior you desire. As is this
is behaving correctly.
This is because the identity of the pushed view changes when you
change the sort order.
As mentioned in my comment above I believe this is a bug in iOS 13.4.
A workaround could be to use a NavigationLink outside of the List and define the List rows as Buttons that
a) set the task to be edited (a new #State var selectedTask) and
b) trigger the NavigationLink to TaskDetail(task: selectedTask!).
This setup will uncouple the selected task from its position in the sorted list thus avoiding the misbehaviour caused by the re-sort potentially caused by editing the dueDate.
To achieve this:
add these two #State variables to struct ContentView
#State private var selectedTask: Task?
#State private var linkIsActive = false
update the body of struct ContentView as follows
var body: some View {
NavigationView {
ZStack {
NavigationLink(
destination: linkDestination(selectedTask: selectedTask),
isActive: self.$linkIsActive) {
EmptyView()
}
List(tasks) { task in
Button(action: {
self.selectedTask = task
self.linkIsActive = true
}) {
NavigationLink(destination: EmptyView()){
Text("\(task.title)")
}
}
}
}
.navigationBarTitle("Tasks").navigationBarItems(trailing: Button("new") {self.addTask()})
}
}
add the following struct to ContentView.swift
struct linkDestination: View {
let selectedTask: Task?
var body: some View {
return Group {
if selectedTask != nil {
TaskDetail(task: selectedTask!)
} else {
EmptyView()
}
}
}
}
I encountered the same problem and could not find a good solution.
But I have another workaround.
I made the Fetchrequest dynamic and changed the sortdescriptor, when the link is active.
This has the negative sideeffect that the list sorts itself with an animation every time you navigate back to the ContentView.
If you add the following struct for your list:
struct TaskList: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest var tasks: FetchedResults<Task>
#Binding var activeTaskLink: Int?
init(activeTaskLink: Binding<Int?>, currentSortKey: String) {
self._activeTaskLink = activeTaskLink
self._tasks = Task.requestAllTask(withSortKey: currentSortKey)
}
var body: some View {
List(tasks, id: \.id) { task in
NavigationLink(destination: TaskDetail(task: task), tag: task.objectId.hashValue, selection: self.$activeTaskLink) {
Text("\(task.title)")
}
}
}
}
Then change the requestAllTask function in Task.swift:
static func requestAllTasks(withSortKey key: String) -> NSFetchRequest<Task> {
let request: NSFetchRequest<Task> = Task.fetchRequest() as! NSFetchRequest<Task>
let sortDescriptor = NSSortDescriptor(key: key, ascending: true)
request.sortDescriptors = [sortDescriptor]
return request
}
Then add a state for the activeTask in the ContentView
#State var activeTaskLink: Int? = nil
and change the body to
var body: some View {
TaskList(activeTaskLink: self.$activeTaskLink, currentSortKey: self.activeNavLink != nil ? "id" : "dueDate")
.navigationBarTitle("Tasks")
.navigationBarItems(trailing: Button("new") {self.addTask()})
}

Pushing a new List from a NavigationLink within a List freezes the simulator/canvas (SwiftUI)

When building a List View that can push a secondary List of items, the XCode simulator and SwiftUI canvas freezes without throwing an error. The code below describes the view hierarchy I am using to recreate this issue:
class Listable: Identifiable {
var id: UUID = UUID()
var name: String = "Name"
var title: String = "Title"
var description: String = "This is a description"
}
struct ContentView: View {
var items: [Listable]
var body: some View {
NavigationView {
List(items) { item in
ListCell(item: item)
}
.navigationBarTitle(self.items[0].title)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(items: testList)
}
}
struct SecondaryListView: View {
var items: [Listable]
var body: some View {
List(items) { item in
ListCell(item: item)
}
.navigationBarTitle(self.items[0].title)
}
}
struct ListCell: View {
var item: Listable
var body: some View {
NavigationLink(destination: SecondaryListView(items: testSecondaryList)) {
Image(systemName: "photo")
VStack(alignment: .leading) {
Text(item.name)
.font(.title)
.fontWeight(.light)
Text(item.description)
}
}
}
}
let testList = [
Listable(),
Listable(),
Listable()
]
let testSecondaryList = [
Listable(),
Listable(),
Listable(),
Listable(),
Listable(),
Listable()
]
Note: If I replace the List object within SecondaryListView with a ForEach (as seen below), the code compiles and runs with no issue, and I can navigate as far down the stack as I'd like.
struct SecondaryListView: View {
var items: [Listable]
var body: some View {
ForEach(items) { item in
ListCell(item: item)
}
.navigationBarTitle(self.items[0].title)
}
}
Is pushing a List View from within a List View not allowed or is this a bug? It appears to cause a memory leak possibly - CPU on my main thread hits 99%.
The root cause ended up being that I was modifying the UITableView.appearance().backgroundView in the SceneDelegate. See below code:
SceneDelegate.swift:
UITableView.appearance().backgroundView = UIImageView(image: UIImage(named:"Background"), highlightedImage: nil)
My goal was to have a background image across all UITableViews within the app (Lists in SwiftUI still leverage UIKit components). The workaround I settled on was instead setting a background color with a UIColor init'd with a pattern image, as shown below:
UITableView.appearance().backgroundColor = UIColor(patternImage: UIImage(named:"Background") ?? UIImage())