I'm trying to implement dynamically provided options for my iOS 15 widget. So far I successfully implemented static intent parameters, and now I wanted to extend it with dynamically provided ones.
The dynamic parameter in my .intentdefinition file is called HotspotList and has type String.
I have defined a struct where I have also saved a list of availableHotspots:
struct Hotspot: Hashable, Identifiable, Codable {
...
static var availableHotspots = UserDefaults.standard.object(forKey: "hotspots") as? [String] ?? []
}
I have checked that this array is successfully saved with print(Hotspot.availableHotspots) somewhere in my main View.
Now I want to use this array in my IntentHandler.swift file:
import Intents
class IntentHandler: INExtension, WidgetConfigurationIntentHandling {
override func handler(for intent: INIntent) -> Any {
return self
}
func provideHotspotListOptionsCollection(for intent: WidgetConfigurationIntent) async throws -> INObjectCollection<NSString> {
let hotspots: [NSString] = Hotspot.availableHotspots.map { element in
let nsstring = element as NSString
return nsstring
}
let collection = INObjectCollection(items: hotspots)
return collection
}
func defaultHotspotList(for intent: WidgetConfigurationIntent) -> String? {
return "thisIsJustATest"
}
}
I see that the intent is implemented correctly, because defaultHotspotList() returns the default parameter. But somehow provideHotspotListOptionsCollection() doesn't return the list of Strings. What am I doing wrong?
Note: I also tried the non-async option of the function:
func provideHotspotListOptionsCollection(for intent: WidgetConfigurationIntent, with completion: #escaping (INObjectCollection<NSString>?, Error?) -> Void) {
let hotspots: [NSString] = Hotspot.availableHotspots.map { element in
let nsstring = element as NSString
return nsstring
}
let collection = INObjectCollection(items: hotspots)
print(collection)
completion(collection, nil)
}
So with the hint of #loremipsum I was able to fix it by myself. For anyone interested, here's my solution:
I added the following lines in my MainView in the App target:
struct MainView: View {
#AppStorage("hotspots", store: UserDefaults(suiteName: "group.com.<<bundleID>>"))
var hotspotData: Data = Data()
...
save(hotspots: miners)
...
func save(hotspots: [String]) {
do {
hotspotData = try NSKeyedArchiver.archivedData(withRootObject: hotspots, requiringSecureCoding: false)
WidgetCenter.shared.reloadAllTimelines()
} catch let error {
print("error hotspots key data not saved \(error.localizedDescription)")
}
}
and then to retrieve the data in the IntentHandler.swift:
import Intents
import SwiftUI
class IntentHandler: INExtension, WidgetConfigurationIntentHandling {
#AppStorage("hotspots", store: UserDefaults(suiteName: "group.com.<<bundleID>>"))
var hotspotData: Data = Data()
func provideHotspotListOptionsCollection(for intent: WidgetConfigurationIntent) async throws -> INObjectCollection<NSString> {
var hotspots: [String] {
var hotspots: [String]?
do {
hotspots = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(hotspotData) as? [String]
} catch let error {
print("color error \(error.localizedDescription)")
}
return hotspots ?? []
}
let NSHotspots: [NSString] = hotspots.map { element in
let nsstring = element as NSString
return nsstring
}
let collection = INObjectCollection(items: NSHotspots)
return collection
}
Related
I am learning Swift to develop macOS applications and I ran into a problem. I am trying to get certain data from a JSON from the internet. I have managed to get such data and put it in simple text labels in the view but when I run Xcode and get the values, if the values from the JSON get updated, I can't see it reflected in my app. I know that I must perform a function to refresh the data but what I have always found is the function to refresh the data that is in a table, not a simple text label.
Regardless of this problem, if I wanted to add a table with 3 columns (each structure has 3 data, at least) with the values from the JSON. When I run the refresh of the table, I should include in the function the function that gets the data from the internet, right? I'm a bit lost with this too.
This is what I have:
ViewController.swift
import Cocoa
class ViewController: NSViewController, NSTextFieldDelegate {
let user_items = UserItems()
#IBOutlet var item_salida: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
let struc_item = user_items.Item_Struct()
let position = struc_item.firstIndex(where: { $0.name == "Leanne Graham" })!
print(struc_item[position].state!)
item_salida.stringValue = struc_item[position].state!
} }
Struct.swift
import Foundation
import SwiftyJSON
// MARK: - Dato
struct User: Codable {
var name: String?
var username: String?
var email: String?
}
typealias Datos = [User]
class UserItems {
func Item_Struct() -> Datos {
let urlString = "https://jsonplaceholder.typicode.com/users"
var items_available: [User] = []
if let url = NSURL(string: urlString){
if let data = try? NSData(contentsOf: url as URL, options: []){
let items = try! JSONDecoder().decode([User].self, from: data as Data)
for item in items {
items_available.append(item)
}
}
}
return items_available
}
}
Thanks, a lot!
First of all - as you are learning Swift - please stop using snake_case variable names and also the NS.. classes NSURL and NSData.
Never load data from a remote URL with synchronous Data(contentsOf. It blocks the thread.
You need URLSession and an asynchronous completion handler.
// MARK: - Dato
struct User: Codable {
let name: String
let username: String
let email: String
}
typealias Datos = [User]
class UserItems {
func loadData(completion: #escaping (Datos) -> Void) {
let url = URL(string: "https://jsonplaceholder.typicode.com/users")!
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error { print(error); return }
do {
let items = try JSONDecoder().decode([User].self, from: data!)
completion(items)
} catch {
print(error)
}
}.resume()
}
}
And use it in the controller
class ViewController: NSViewController, NSTextFieldDelegate {
#IBOutlet var itemSalida: NSTextField!
let userItems = UserItems()
override func viewDidLoad() {
super.viewDidLoad()
userItems.loadData { users in
if let position = users.firstIndex(where: { $0.name == "Leanne Graham" }) {
DispatchQueue.main.async {
print(users[position].username)
self.itemSalida.stringValue = users[position].username
}
}
}
}
}
And forget SwiftyJSON. It's not needed anymore in favor of Codable.
I have the following code, How can i accomplish this without changing struct into class. Escaping closure captures mutating 'self' parameter,
struct RegisterView:View {
var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
self.names = name //here is the error
}){(error) in
print("Error: \(error)")
}
init(){
LoadPerson()
}a
var body:some View{
//ui code
}
}
Firebasemanager.swift
struct FirebaseManager {
func fetchPerson(
success: #escaping (Person) -> (),
failure: #escaping (String) -> ()
) {
Database.database().reference().child("Person")
.observe(.value, with: { (snapshot) in
if let dictionary = snapshot.value as? [String: Any] {
success(Person(dictionary: dictionary))
}
}) { (error) in
failure(error.localizedDescription)
}
}
}
SwiftUI view can be created (recreated) / copied many times during rendering cycle, so View.init is not appropriate place to load some external data. Use instead dedicated view model class and load explicitly only when needed.
Like
class RegisterViewModel: ObservableObject {
#Published var names = [String]()
func loadPerson() {
// probably it also worth checking if person has already loaded
// guard names.isEmpty else { return }
FirebaseManager.fetchNames(success:{(person) in
guard let name = person.name else {return}
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
struct RegisterView: View {
// in SwiftUI 1.0 it is better to inject view model from outside
// to avoid possible recreation of vm just on parent view refresh
#ObservedObject var vm: RegisterViewModel
// #StateObject var vm = RegisterViewModel() // << only SwiftUI 2.0
var body:some View{
Some_Sub_View()
.onAppear {
self.vm.loadPerson()
}
}
}
Make the names property #State variable.
struct RegisterView: View {
#State var names = [String]()
private func LoadPerson(){
FirebaseManager.fetchNames(success: { person in
guard let name = person.name else { return }
DispatchQueue.main.async {
self.names = [name]
}
}){(error) in
print("Error: \(error)")
}
}
//...
}
Here, we have a scenario where I am facing issue for parsing the model class using "JSONDecoder".
Let me share you what I have done in this example and where I am facing issue:
There's a model protocol derived through the Codable.
A struct model and A class GenericExample
GenericExample class have following things:
shared instance
typeContainer which is the type of Protocol
There's two more methods staticClassParsing and dynamicClassParsing with one parameter dynamicType
In the last, One generic method which return the parsed data model.
Calling method for parsing the model:
GenericExample.shared.dynamicClassParsing(Numbers.self,data: "[\r\n{\"one\": \"1\"},\r\n{\"two\":\"2\"}\r\n]")
Compile Time Error:
Generic parameter 'T' could not be inferred
Occurred here
returnModelType: typeContainer.self
**For more details, please go through following code: **
protocol BaseMapModel : Codable { }
struct Numbers: BaseMapModel {
var one: String?
var two: String?
}
class GenericExample: NSObject {
static let shared = GenericExample()
var typeContainer : BaseMapModel.Type?
private override init() {
super.init()
}
}
extension GenericExample {
// Static Class Parsing passed through the conversion
func staticClassParsing() {
let dataJson = "[\r\n{\"one\": \"1\"},\r\n{\"two\":\"2\"}\r\n]"
convertTypeContainer(data: Data(dataJson.utf8), returnModelType: Numbers.self) { (mappedResult) in
print(mappedResult?.one ?? "")
print(mappedResult?.two ?? "")
}
}
// Dynamic Class Parsing can't passed through the conversion
// Error:- Generic parameter 'T' could not be inferred
// Error Parameter:- in "returnModelType: typeContainer.self"
func dynamicClassParsing(_ dynamicType: BaseMapModel.Type, data:String) {
typeContainer = dynamicType.self
convertTypeContainer(data: Data(data.utf8), returnModelType: typeContainer.self) { (mappedResult) in
print(mappedResult?.one ?? "")
print(mappedResult?.two ?? "")
}
}
}
extension GenericExample {
private func convertTypeContainer<T : BaseMapModel>(data:Data, returnModelType: T.Type, completion: ((_ result:T?)->Void)) {
guard let responseModel = try? JSONDecoder().decode(returnModelType.self, from: data) else {
completion(nil)
return
}
completion(responseModel)
}
}
typeContainer must be a concrete type, it cannot be a protocol. And the completion handler is pointless as JSONDecoder works synchronously.
To decode JSON with generics you have to use something like this, it's highly recommended to handle also the Decoding error
struct Numbers: Decodable {
var one: String?
var two: String?
}
class GenericExample: NSObject {
static let shared = GenericExample()
}
extension GenericExample {
func dynamicClassParsing<T : Decodable>(_ dynamicType: T.Type, data: String) -> Result<T,Error> {
return Result { try JSONDecoder().decode(T.self, from: Data(data.utf8)) }
}
}
let dataJson = """
[{"one": "1"},{"two":"2"}]
"""
let result = GenericExample.shared.dynamicClassParsing([Numbers].self, data: dataJson)
switch result {
case .success(let numbers): print(numbers)
case .failure(let error): print(error)
}
First of all, Thanks to All for your support. And Yes, I would like to post answer to my question.
class BaseMapModel : Codable { }
class Numbers: BaseMapModel {
var one: String?
var two: String?
enum CodingKeys: String, CodingKey {
case one = "one"
case two = "two"
}
// Decoding
required init(from decoder: Decoder) throws {
let response = try decoder.container(keyedBy: CodingKeys.self)
one = try? response.decode(String.self, forKey: .one)
two = try? response.decode(String.self, forKey: .two)
let superDecoder = try response.superDecoder()
try super.init(from: superDecoder)
}
}
class GenericExample: NSObject {
static let shared = GenericExample()
var defaultTypeContainer : Numbers.Type!
var typeContainer : BaseMapModel.Type?
private override init() {
super.init()
}
}
extension GenericExample {
// Static Class Parsing passed through the conversion
func staticClassParsing() {
let dataJson = "[\r\n{\"one\": \"1\"},\r\n{\"two\":\"2\"}\r\n]"
convertTypeContainer(data: Data(dataJson.utf8), returnModelType: Numbers.self) { (mappedResult) in
print(mappedResult?.one ?? "")
print(mappedResult?.two ?? "")
}
}
// Dynamic Class Parsing passed through the conversion
func dynamicClassParsing(_ dynamicType: BaseMapModel.Type, data:String) {
typeContainer = dynamicType.self
convertTypeContainer(data: Data(data.utf8), returnModelType: (typeContainer ?? defaultTypeContainer).self) { (mappedResult) in
print((mappedResult as? Numbers)?.one ?? "")
print((mappedResult as? Numbers)?.two ?? "")
}
}
}
extension GenericExample {
private func convertTypeContainer<T : BaseMapModel>(data:Data, returnModelType: T.Type, completion: ((_ result:T?)->Void)) {
guard let responseModel = try? JSONDecoder().decode(returnModelType.self, from: data) else {
completion(nil)
return
}
completion(responseModel)
}
}
Im trying to save struct array into UserDefaults and I cant figure out why JsonEncoder return empty data. I have setup model that conforms Codable protocol
struct MenuItem : Codable{
let name : String?
let icon : String?
init(name : String?, icon : String?){
self.name = name
self.icon = icon
}
}
and also created Defaults manager for saving it into user defaults.
class DefaultsManager {
static let shared = DefaultsManager()
init(){}
var items: [MenuItem]{
get{
if let json = UserDefaults.standard.data(forKey: "key"){
return decodeFromJson(jsonData: json)
} else {
return []
}
}
set{
let json = codeToJson(data: items)
UserDefaults.standard.set(json, forKey: "key")
}
}
fileprivate func codeToJson<T:Codable>(data: Array<T>) -> Data?{
do {
return try JSONEncoder().encode(data)
} catch {
print(error)
return nil
}
}
fileprivate func decodeFromJson<T:Codable>(jsonData: Data) -> [T]{
do {
return try JSONDecoder().decode(Array<T>.self, from: jsonData)
} catch {
print(error)
return []
}
}
}
but whatever I do JsonEncoder returns empty data.. I tried to google but without success.
That's a very common mistake.
In a setter of a computed property the new value is represented by the implicit newValue variable, it's not the property itself.
set {
let json = codeToJson(data: newValue)
UserDefaults.standard.set(json, forKey: "key")
}
Change Array<T>.self to [MenuItem].self
return try JSONDecoder().decode([MenuItem].self, from: jsonData)
I am new to MVC design pattern. I created "DataModel" it will make an API call, create data, and return data to the ViewController using Delegation and "DataModelItem" that will hold all data. How to call a DataModel init function in "requestData" function. Here is my code:
protocol DataModelDelegate:class {
func didRecieveDataUpdata(data:[DataModelItem])
func didFailUpdateWithError(error:Error)
}
class DataModel: NSObject {
weak var delegate : DataModelDelegate?
func requestData() {
}
private func setDataWithResponse(response:[AnyObject]){
var data = [DataModelItem]()
for item in response{
if let tableViewModel = DataModelItem(data: item as? [String : String]){
data.append(tableViewModel)
}
}
delegate?.didRecieveDataUpdata(data: data)
}
}
And for DataModelItem:
class DataModelItem{
var name:String?
var id:String?
init?(data:[String:String]?) {
if let data = data, let serviceName = data["name"] , let serviceId = data["id"] {
self.name = serviceName
self.id = serviceId
}
else{
return nil
}
}
}
Controller:
class ViewController: UIViewController {
private let dataSource = DataModel()
override func viewDidLoad() {
super.viewDidLoad()
dataSource.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
dataSource.requestData()
}
}
extension ViewController : DataModelDelegate{
func didRecieveDataUpdata(data: [DataModelItem]) {
print(data)
}
func didFailUpdateWithError(error: Error) {
print("error: \(error.localizedDescription)")
}
}
How to implement simple MVC design pattern in Swift?
As a generic answer, in iOS development you're already doing this implicitly! Dealing with storyboard(s) implies the view layer and controlling the logic of how they work and how they are connected to the model is done by creating view controller, that's the default flow.
For your case, let's clarify a point which is: according to the standard MVC, by default the responsible layer for calling an api should be -logically- the view controller. However for the purpose of modularity, reusability and avoiding to create massive view controllers we can follow the approach that you are imitate, that doesn't mean that its the model responsibility, we can consider it a secondary helper layer (MVC-N for instance), which means (based on your code) is DataModel is not a model, its a "networking" layer and DataModelItem is the actual model.
How to call a DataModel init function in "requestData" function
It seems to me that it doesn't make scene. What do you need instead is an instance from DataModel therefore you could call the desired method.
In the view controller:
let object = DataModel()
object.delegate = self // if you want to handle it in the view controller itself
object.requestData()
I am just sharing my answer here and I am using a codable. It will be useful for anyone:
Model:
import Foundation
struct DataModelItem: Codable{
struct Result : Codable {
let icon : String?
let name : String?
let rating : Float?
let userRatingsTotal : Int?
let vicinity : String?
enum CodingKeys: String, CodingKey {
case icon = "icon"
case name = "name"
case rating = "rating"
case userRatingsTotal = "user_ratings_total"
case vicinity = "vicinity"
}
}
let results : [Result]?
}
NetWork Layer :
import UIKit
protocol DataModelDelegate:class {
func didRecieveDataUpdata(data:[String])
func didFailUpdateWithError(error:Error)
}
class DataModel: NSObject {
weak var delegate : DataModelDelegate?
var theatreNameArray = [String]()
var theatreVicinityArray = [String]()
var theatreiconArray = [String]()
func requestData() {
Service.sharedInstance.getClassList { (response, error) in
if error != nil {
self.delegate?.didFailUpdateWithError(error: error!)
} else if let response = response{
self.setDataWithResponse(response: response as [DataModelItem])
}
}
}
private func setDataWithResponse(response:[DataModelItem]){
for i in response[0].results!{
self.theatreNameArray.append(i.name!)
self.theatreVicinityArray.append(i.vicinity!)
self.theatreiconArray.append(i.icon!)
}
delegate?.didRecieveDataUpdata(data: theatreNameArray)
print("TheatreName------------------------->\(self.theatreNameArray)")
print("TheatreVicinity------------------------->\(self.theatreVicinityArray)")
print("Theatreicon------------------------->\(self.theatreiconArray)")
}
}
Controller :
class ViewController: UIViewController {
private let dataSource = DataModel()
override func viewDidLoad() {
super.viewDidLoad()
dataSource.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
dataSource.requestData()
}
}
extension ViewController : DataModelDelegate{
func didRecieveDataUpdata(data: [DataModelItem]) {
print(data)
}
func didFailUpdateWithError(error: Error) {
print("error: \(error.localizedDescription)")
}
}
APIManager :
class Service : NSObject{
static let sharedInstance = Service()
func getClassList(completion: (([DataModelItem]?, NSError?) -> Void)?) {
guard let gitUrl = URL(string: "") else { return }
URLSession.shared.dataTask(with: gitUrl) { (data, response
, error) in
guard let data = data else { return }
do {
let decoder = JSONDecoder()
let gitData = try decoder.decode(DataModelItem.self, from: data)
completion!([gitData],nil)
} catch let err {
print("Err", err)
completion!(nil,err as NSError)
}
}.resume()
}
}
I would recommend using a singleton instance for DataModel, since this would be a class you would be invoking from many points in your application.
You may refer its documentation at :
Managing Shared resources using singleton
With this you wont need to initialise this class instance every time you need to access data.