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())
}
}
}
Related
Are there any better syntax for generics? Belowed code gives me huge ugly generics definition in the beginning of view.
struct ViewUserSettings<TypeGif: ModelGifProtocol, TypeFont: ModelFontProtocol, TypeMagicText: ModelMagicTextProtocol, TypePreSetAnswer: ModelPreSetAnswerProtocol>: View where TypeGif: Hashable, TypeFont: Hashable, TypeMagicText: Hashable, TypePreSetAnswer: Hashable
I had to define Hashable inside generics otherwise compiler gives Error when I tried to use this Protocol in SwiftUI ForEach.
ModelGif.swift
// MARK: - Protocol
enum GifType: String, Codable {
case fooA = "fooA more"
case fooB = "fooB more"
case fooC = "fooC more"
// MARK: Properties
var fileName600px: String {
switch self {
case .fooA: return "fooAHQ"
case .fooB: return "fooBHQ"
case .fooC: return "fooCHQ"
}
}
var fileName80px: String {
switch self {
case .fooA: return "fooALQ"
case .fooB: return "fooBLQ"
case .fooC: return "fooCLQ"
}
}
}
protocol ModelGifProtocol: Codable {
var gif: GifType { get }
}
// MARK: - Module
struct ModelGif: ModelGifProtocol, Hashable {
// MARK: Properties
let gif: GifType
// MARK: Initialization
init(_ gif: GifType) {
self.gif = gif
}
// MARK: Static Properties
static var fooA = Self(.fooA)
static var fooB = Self(.fooB)
static var fooC = Self(.fooC)
}
ModelMagicText.swift
// MARK: - Protocol
protocol ModelMagicTextProtocol: Codable {
var id: UUID { get }
var context: String { get }
init(_ context: String, id: UUID) throws
init(_ context: String) throws
}
// MARK: - Module
struct ModelMagicText: ModelMagicTextProtocol, Hashable {
// MARK: Properties
let id: UUID
let context: String
// MARK: Initialization
init(_ context: String, id: UUID) throws {
guard context.count <= 30 else { throw ContextError.tooLong } // restricted at ViewAnimatingCircleText.modifyDynamicText()
self.context = context.uppercased()
self.id = id
}
init(_ context: String) throws {
try self.init(context, id: UUID())
}
init(_ model: ModelMagicTextProtocol) throws {
try self.init(model.context, id: model.id)
}
// MARK: Static Properties
static let defaultText = try! ModelMagicText("foooooooooooo")
}
// MARK: - Extension: ContextError
extension ModelMagicText {
enum ContextError: Error {
case tooLong
}
}
We can format that more readably using multiple lines. We can also move the Hashable constraints into the generic parameter list:
struct ViewUserSettings<
TypeGif: ModelGifProtocol & Hashable,
TypeFont: ModelFontProtocol & Hashable,
TypeMagicText: ModelMagicTextProtocol & Hashable,
TypePreSetAnswer: ModelPreSetAnswerProtocol & Hashable
>: View {
var gif: TypeGif
var font: TypeFont
var magicText: TypeMagicText
var preSetAnswer: TypePreSetAnswer
var body: some View { EmptyView() }
}
You only need a where clause when you have equality constraints, but your example code doesn't have any equality constraints.
If you change each of your model protocols to inherit from Hashable, you can eliminate the Hashable constraints entirely:
protocol ModelGifProtocol: Hashable { }
protocol ModelFontProtocol: Hashable { }
protocol ModelMagicTextProtocol: Hashable { }
protocol ModelPreSetAnswerProtocol: Hashable { }
struct ViewUserSettings<
TypeGif: ModelGifProtocol,
TypeFont: ModelFontProtocol,
TypeMagicText: ModelMagicTextProtocol,
TypePreSetAnswer: ModelPreSetAnswerProtocol
>: View {
...
There's also the question of whether you need such verbose names. Do you really need the Type and Model prefixes?
protocol GifProtocol: Hashable { }
protocol FontProtocol: Hashable { }
protocol MagicTextProtocol: Hashable { }
protocol PreSetAnswerProtocol: Hashable { }
struct ViewUserSettings<
Gif: GifProtocol,
Font: FontProtocol,
MagicText: MagicTextProtocol,
PreSetAnswer: PreSetAnswerProtocol
>: View {
var gif: Gif
var font: Font
var magicText: MagicText
var preSetAnswer: PreSetAnswer
var body: some View { EmptyView() }
}
Keep in mind that you can still refer to SwiftUI's Font type as SwiftUI.Font inside ViewUserSettings.
You didn't give any details regarding how your protocols are defined. Do you really need your own ModelFontProtocol? SwiftUI's Font is already Hashable, so if you can just use the concrete Font type, your code will be simpler. Maybe you can use concrete types in place of your other protocols too, but we can't say without seeing how they're defined.
If you're using the same set of type parameters and constraints for multiple views, there are ways to factor the common stuff out. You should edit your question to include more example code if that's the case.
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
API call and JSON decoding are working fine, as I can print to console any item from the JSON data set without a problem.
Here's API call and test print:
import Foundation
import SwiftUI
import Combine
class APICall : ObservableObject {
#Published var summary: Summary?
init () {
pullSummary()
}
func pullSummary() {
let urlCall = URL(string: "https://api.covid19api.com/summary")
guard urlCall != nil else {
print("Error reaching API")
return
}
let session = URLSession.shared
let dataTask = session.dataTask(with: urlCall!) { (data, response, error) in
if error == nil && data != nil {
let decoder = JSONDecoder()
do {
let summary = try decoder.decode(Summary.self, from: data!)
print(summary.byCountry[40].cntry as Any)
DispatchQueue.main.async {
self.summary = summary
}
}
catch {
print("Server busy, try again in 5 min.")
}
}
}
dataTask.resume()
}
}
And here is the structure of the "Summary" data model used for the decoding and data object structure:
import Foundation
struct Summary: Decodable {
let global: Global
let byCountry: [ByCountry]
let date: String
enum CodingKeys: String, CodingKey {
case global = "Global"
case byCountry = "Countries"
case date = "Date"
}
struct Global: Decodable {
let globalNC: Int
let globalTC: Int
let globalND: Int
let globalTD: Int
let globalNR: Int
let globalTR: Int
enum CodingKeys: String, CodingKey {
case globalNC = "NewConfirmed"
case globalTC = "TotalConfirmed"
case globalND = "NewDeaths"
case globalTD = "TotalDeaths"
case globalNR = "NewRecovered"
case globalTR = "TotalRecovered"
}
}
struct ByCountry: Decodable {
let cntry: String?
let ccode: String
let slug: String
let cntryNC: Int
let cntryTC: Int
let cntryND: Int
let cntryTD: Int
let cntryNR: Int
let cntryTR: Int
let date: String
enum CodingKeys: String, CodingKey {
case cntry = "Country"
case ccode = "CountryCode"
case slug = "Slug"
case cntryNC = "NewConfirmed"
case cntryTC = "TotalConfirmed"
case cntryND = "NewDeaths"
case cntryTD = "TotalDeaths"
case cntryNR = "NewRecovered"
case cntryTR = "TotalRecovered"
case date = "Date"
}
}
}
As shown, the results of the API call and JSON decode are published as required using ObserveableObject and #Published.
Over in the ContentView, I have followed the ObservedObject rules and only want to display on the UI a data point from the JSON data to confirm it's working:
import SwiftUI
import Foundation
import Combine
struct ContentView: View {
#ObservedObject var summary = APICall()
var body: some View {
Text($summary.date)
.onAppear {
self.summary.pullSummary()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
}
BUT... I get these 2 errors at the Text display line, 1) Initializer 'init(_:)' requires that 'Binding<Subject>' conform to 'StringProtocol' and 2) Value of type 'ObservedObject<APICall>.Wrapper' has no dynamic member 'date' using the key path from root type 'APICall'.
I am guessing the 2nd error is the root cause of the problem, indicating the data is not being passed into the ContentView correctly.
I appreciate any suggestions.
Thanks.
It is messed view model with internal property
struct ContentView: View {
#ObservedObject var viewModel = APICall()
var body: some View {
Text(viewModel.summary?.date ?? "Loading...") // << no $ sign !!!
.onAppear {
self.viewModel.pullSummary()
}
}
}
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 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)
}
}