I'm trying to create a list from an array of Request objects.
I have defined a custom view RequestRow to display a Request.
The following works to display a list of requests…
struct RequestsView : View {
let requests: [Request]
var body: some View {
List {
ForEach(0..<requests.count) { i in
RequestRow(request: self.requests[i])
}
}
}
}
but the following won't compile…
struct RequestsView : View {
let requests: [Request]
var body: some View {
List {
ForEach(requests) { request in
RequestRow(request: request)
}
}
}
}
Cannot convert value of type '(Request) -> RequestRow' to expected argument type '(_) -> _'
Any thoughts as to what the issue might be?
OK, I soon figured out the answer. Per the Apple's docs, the array elements must be Identifiable, so this works…
var body: some View {
List {
ForEach(requests.identified(by: \.self)) { request in
RequestRow(request: request)
}
}
}
I'm sure I won't be the last person to have this problem, so I'll leave this here for future reference.
I have the same problem, but in my case Component is a Protocol, so it can't be conformed to Identifiable
VStack {
ForEach(components.identified(by: \.uuid)) { value -> UIFactory in
UIFactory(component: value)
}
}
However, If I try something like this it works fine
VStack {
ForEach(components.identified(by: \.uuid)) { value -> UIFactory in
Text(value.uuid)
}
}
Related
To provide some context, Im writing an order tracking section of our app, which reloads the order status from the server every so-often. The UI on-screen is developed in SwiftUI. I require an optional image on screen that changes as the order progresses through the statuses.
When I try the following everything works...
My viewModel is an ObservableObject:
internal class MyAccountOrderViewModel: ObservableObject {
This has a published property:
#Published internal var graphicURL: URL = Bundle.main.url(forResource: "tracking_STAGEONE", withExtension: "gif")!
In SwiftUI use the property as follows:
GIFViewer(imageURL: $viewModel.graphicURL)
My issue is that the graphicURL property has a potentially incorrect placeholder value, and my requirements were that it was optional. Changing the published property to: #Published internal var graphicURL: URL? causes an issue for my GIFViewer which rightly does not accept an optional URL:
Cannot convert value of type 'Binding<URL?>' to expected argument type 'Binding<URL>'
Attempting the obvious unwrapping of graphicURL produces this error:
Cannot force unwrap value of non-optional type 'Binding<URL?>'
What is the right way to make this work? I don't want to have to put a value in the property, and check if the property equals placeholder value (Ie treat that as if it was nil), or assume the property is always non-nil and unsafely force unwrap it somehow.
Below is an extension of Binding you can use to convert a type like Binding<Int?> to Binding<Int>?. In your case, it would be URL instead of Int, but this extension is generic so will work with any Binding:
extension Binding {
func optionalBinding<T>() -> Binding<T>? where T? == Value {
if let wrappedValue = wrappedValue {
return Binding<T>(
get: { wrappedValue },
set: { self.wrappedValue = $0 }
)
} else {
return nil
}
}
}
With example view:
struct ContentView: View {
#StateObject private var model = MyModel()
var body: some View {
VStack(spacing: 30) {
Button("Toggle if nil") {
if model.counter == nil {
model.counter = 0
} else {
model.counter = nil
}
}
if let binding = $model.counter.optionalBinding() {
Stepper(String(binding.wrappedValue), value: binding)
} else {
Text("Counter is nil")
}
}
}
}
class MyModel: ObservableObject {
#Published var counter: Int?
}
Result:
An example function:
#ViewBuilder func returnView() -> some View {
if thisIsTrue == true {
SomeView()
} else {
AnotherView()
}
}
I've tried testing like this:
let testView = sut.returnView()
XCTAssert(testView is SomeView)
Which passes when there is only one possible type of view, but then fails as soon as there is a choice.
Any suggestions as to how I can unit test the output of this function?
The opaque return type some View means this function always returns exactly one type on all paths our of the function and that type conforms to View, so while it looks like you are returning two different things the ViewBuilder in fact collapses this into a single type that is generic with respect to the real return type. If you want to know what the opaque type really is you can just have the compiler tell you. For instance here is a playground. Note that this solution is fragile because changing the implementation of the function very likely will change the return type.
import SwiftUI
struct SomeView: View {
var body: some View { EmptyView() }
}
struct AnotherView: View {
var body: some View { Color.red}
}
#ViewBuilder func returnView() -> some View {
if true {
SomeView()
} else {
AnotherView()
}
}
let a = returnView()
print(type(of: a))
output:
_ConditionalContent<SomeView, AnotherView>
The solution that I went with was to not unit test the output of the function at all.
I created an enum in the view model that had cases that mapped to the different views and then used a computed property of this type to separate the business logic from the view logic.
enum ViewType {
case someView
case anotherView
}
var viewType: ViewType {
if thisIsTrue {
return .someView
} else {
return .anotherView
}
}
I can instantiate and test this in my unit testing.
Then in the view itself I created an #ViewBuilder variable and used a switch statement to map it to the view model viewType:
#ViewBuilder var view: some View {
switch viewModel.viewType {
case .someView:
SomeView()
case .anotherView:
AnotherView()
}
}
I hope this is helpful to someone else.
When creating a class conforming to ReferenceFileDocument, how do you indicate the document needs saving. i.e. the equivalent of the NSDocument's updateChangeCount method?
I've met the same problem that the SwiftUI ReferenceFileDocument cannot trigger the update. Recently, I've received feedback via the bug report and been suggested to register an undo.
Turns out the update of ReferenceFileDocument can be triggered, just like UIDocument, by registering an undo action. The difference is that the DocumentGroup explicitly implicitly setup the UndoManager via the environment.
For example,
#main
struct RefDocApp: App {
var body: some Scene {
DocumentGroup(newDocument: {
RefDocDocument()
}) { file in
ContentView(document: file.document)
}
}
}
struct ContentView: View {
#Environment(\.undoManager) var undoManager
#ObservedObject var document: RefDocDocument
var body: some View {
TextEditor(text: Binding(get: {
document.text
}, set: {
document.text = $0
undoManager?.registerUndo(withTarget: document, handler: {
print($0, "undo")
})
}))
}
}
I assume at this stage, the FileDocument is actually, on iOS side, a wrapper on top of the UIDocument, the DocumentGroup scene explicitly implicitly assign the undoManager to the environment. Therefore, the update mechanism is the same.
The ReferenceFileDocument is ObservableObject, so you can add any trackable or published property for that purpose. Here is a demo of possible approach.
import UniformTypeIdentifiers
class MyTextDocument: ReferenceFileDocument {
static var readableContentTypes: [UTType] { [UTType.plainText] }
func snapshot(contentType: UTType) throws -> String {
defer {
self.modified = false
}
return self.storage
}
#Published var modified = false
#Published var storage: String = "" {
didSet {
self.modified = true
}
}
}
ReferenceFileDocument exists for fine grained controll over the document. In comparison, a FileDocument has to obey value semantics which makes it very easy for SwiftUI to implement the undo / redo functionality as it only needs to make a copy before each mutation of the document.
As per the documentation of the related DocumentGroup initializers, the undo functionality is not provided automatically. The DocumentGroup will inject an instance of an UndoManger into the environment which we can make use of.
However an undo manager is not the only way to update the state of the document. Per this documentation AppKit and UIKit both have the updateChangeCount method on their native implementation of the UI/NSDocument object. We can reach this method by grabbing the shared document controller on macOS from within the view and finding our document. Unfortunately I don't have a simple solution for the iOS side. There is a private SwiftUI.DocumentHostingController type which holds a reference to our document, but that would require mirroring into the private type to obtain the reference to the native document, which isn't safe.
Here is a full example:
import SwiftUI
import UniformTypeIdentifiers
// DOCUMENT EXAMPLE
extension UTType {
static var exampleText: UTType {
UTType(importedAs: "com.example.plain-text")
}
}
final class MyDocument: ReferenceFileDocument {
// We add `Published` for automatic SwiftUI updates as
// `ReferenceFileDocument` refines `ObservableObject`.
#Published
var number: Int
static var readableContentTypes: [UTType] { [.exampleText] }
init(number: Int = 42) {
self.number = number
}
init(configuration: ReadConfiguration) throws {
guard
let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8),
let number = Int(string)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.number = number
}
func snapshot(contentType: UTType) throws -> String {
"\(number)"
}
func fileWrapper(
snapshot: String,
configuration: WriteConfiguration
) throws -> FileWrapper {
// For the sake of the example this force unwrapping is considered as safe.
let data = snapshot.data(using: .utf8)!
return FileWrapper(regularFileWithContents: data)
}
}
// APP EXAMPLE FOR MACOS
#main
struct MyApp: App {
var body: some Scene {
DocumentGroup.init(
newDocument: {
MyDocument()
},
editor: { file in
ContentView(document: file.document)
.frame(width: 400, height: 400)
}
)
}
}
struct ContentView: View {
#Environment(\.undoManager)
var _undoManager: UndoManager?
#ObservedObject
var document: MyDocument
var body: some View {
VStack {
Text(String("\(document.number)"))
Button("randomize") {
if let undoManager = _undoManager {
let currentNumber = document.number
undoManager.registerUndo(withTarget: document) { document in
document.number = currentNumber
}
}
document.number = Int.random(in: 0 ... 100)
}
Button("randomize without undo") {
document.number = Int.random(in: 0 ... 100)
// Let the system know that we edited the document, which will
// eventually trigger the auto saving process.
//
// There is no simple way to mimic this on `iOS` or `iPadOS`.
let controller = NSDocumentController.shared
if let document = controller.currentDocument {
// On `iOS / iPadOS` change the argument to `.done`.
document.updateChangeCount(.changeDone)
}
}
}
}
}
Unfortunatelly SwiftUI (v2 at this moment) does not provide a native way to mimic the same functionality, but this workaround is still doable and fairly consice.
Here is a gist where I extended the example with a custom DocumentReader view and a DocumentProxy which can be extended for common document related operations for more convenience: https://gist.github.com/DevAndArtist/eb7e8aa5e7134610c20b1a7aca358604
Im trying to fetch data from a url once ive pressed a button and called for the function but once the function is called i keep getting a typeMismatch error.
This is my code:
struct User: Decodable {
var symbol: String
var price: Double
}
struct Response: Decodable {
var results:[User]
}
struct ContentView: View {
var body: some View {
VStack {
Text("hello")
Button(action: {
self.fetchUsers(amount: 0)
}) {
Text("Button")
}
}
}
func fetchUsers(amount: Int) {
let url:URL = URL(string: "https://api.binance.com/api/v3/ticker/price")!
URLSession.shared.dataTask(with: url) { (data, res, err) in
if let err = err { print(err) }
guard let data = data else { return }
do {
let response = try JSONDecoder().decode(Response.self, from: data)
print(response.results[0])
} catch let err {
print(err)
}
}.resume()
}
}
This is the error:
typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))
The website url where im trying to fetch data from is:
https://api.binance.com/api/v3/ticker/price
Im trying to fetch a specific price from a specific symbol for example the price of ETHBTC, which would be 0.019...
Thank you
There are two mistake in this approach. First of all, if you created a Response struct with
results = [User]
this way you expect the json to be in the form of [result: {}] but you have [{}] format without a name at the beginging. So you should replace the response struct with
typealias Response = [User]
Second of all the API you are using is returning string instead of double as a price, so you should modify your struct to this:
struct User: Decodable {
var symbol: String
var price: String
}
This way it worked for me. Tested under
swift 5
xcode 11.3.1
iOS 13.3.1 non beta
I'm trying to implement an app where the response from a server call is used to build a view. The view can be one of 5 or 6 different types, depending on the data that comes back, all of which have different requirements for the shape and type of the data that gets passed in to them. Whats the best way to define the struct/class for the incoming data? The only way I've been able to make it work so far is by using :Any as the data type
This is the broad strokes of what I'm trying to do...
struct PageViewData {
type: String
id: Int
viewData: Any
}
struct MyViewA: View {
type: String
id: Int
viewData: MyViewADataShape
var body: some View {
//contents here
}
}
struct MyViewADataShape {
navigation: [NavigationItem]
cta: String
}
struct MyViewB: View {
type: String
id: Int
viewData: MyViewBDataShape
var body: some View {
//contents here
}
}
struct MyViewBDataShape {
pageTitle: String
author: String
wordCount: Int
}
var serverResponse: PageViewData = fetchDataFromServer()
if(serverResponse.type == "A") {
MyViewA(serverResponse)
}
if(serverResponse.type == "B") {
MyViewB(serverResponse)
}
As Yonat mentioned, an enum works well here:
struct PageView: View {
enum Response {
case something(DecodableResponseA)
case orOther(DecodableResponseB)
}
// if you were using id + type to determine what your response was, they are
// unnecessary now, but only you know what you were using them for
let id: Int
let response: Response
var body: some View {
self.viewForResponse(response)
}
private func viewForResponse(_ response: Response) -> some View {
switch response {
case .something(let somethingResponse): return AnyView(SomethingView())
case .orOther(let orOtherResponse): return AnyView(OtherView())
}
}
}