I'm trying to create a custom PickerView that gets it's data from an API call to a web-server. The problem I'm having is saving the parsed data into an external variable so that the PickerView protocol methods can access it.
// API Call / Parsing using Alamofire + Unbox
static func makeApiCall(completionHandler: #escaping (CustomDataStructure) -> ()) {
Alamofire.request(webserverUrl, method: .get).responseObject { (response: DataResponse<Experiment>) in
switch response.result {
case .success:
if var configParams = response.result.value {
let inputConfigs = removeExtrasParams(experiment: response.result.value!)
let modifiedViewModel = modifyViewModel(experiment: &configParams, inputConfigs: inputConfigs)
completionHandler(modifiedViewModel)
}
case .failure(_):
break
}
}
}
// Custom PickerClass
class CustomPickerView: UIPickerView {
fileprivate var customDS: CustomDataStructure?
override init() {
super.init()
dataSource = self
delegate = self
SomeClass.makeApiCall(completionHandler: { customds in
self.customDS = customds
})
}
...
}
extension CustomPickerView: UIPickerViewDelegate {
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if let customds = customDS {
if let customDSValues = customds.inputs.first?.value {
return customDSValues[row]
}
}
return "apple"
}
}
extension CustomPickerView: UIPickerViewDataSource {
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if let customds = customDS {
return customds.inputs.values.count
} else {
return 0
}
}
}
The problem I'm having is that customDS returns nil everytime.
What am I doing wrong here?
In the completion block of makeApiCall simply reload your pickerView's component on main thread and you all set to go.
SomeClass.makeApiCall(completionHandler: { customds in
self.customDS = customds
DispatchQueue.main.async {
self.reloadComponent(0)
}
})
Related
I am trying to call model form #main App where the model has the dependency on a repository with init function. The repository has the URLSession and Baseurl properties . I have passed the required property on both approach ..
Here is approach I have tried based on Xcode suggestions ..
#main
struct HomwWorkWithSwiftUIApp: App {
#StateObject var model = FruitsModel(fruitRepository: FruitsRepository.self as! FruitsRepository)
var body: some Scene {
WindowGroup {
ContentView().environmentObject(model)
}
}
}
As a result as was crashed at run time with error Thread 1: signal SIGABRT
The second approach is passing the require parameters like this ..
#main
struct HomwWorkWithSwiftUIApp: App {
#StateObject var model = FruitsModel(fruitRepository: RealFruitsRepository(session: URLSession, baseURL: EndPoint.baseUrl))
var body: some Scene {
WindowGroup {
ContentView().environmentObject(model)
}
}
}
It giving error ..Cannot convert value of type 'URLSession.Type' to expected argument type 'URLSession'
Here is attempt for URLSession instance.
#main
struct HomwWorkWithSwiftUIApp: App {
init() {
}
var url : URLSession
init(url: URLSession) {
self.url = url
}
#StateObject var model = FruitsModel(fruitRepository: RealFruitsRepository(session: url, baseURL: EndPoint.baseUrl))
var body: some Scene {
WindowGroup {
ContentView().environmentObject(model)
}
}
}
Here is the screenshot ..
Here is the repository code ..
import Foundation
protocol FruitsRepository: WebRepository {
func loadFruits() async throws -> [Fruits]
}
struct RealFruitsRepository: FruitsRepository {
let session: URLSession
let baseURL: String
init(session: URLSession, baseURL: String) {
self.session = session
self.baseURL = baseURL
}
func loadFruits() async throws -> [Fruits] {
guard let request = try? API.allFruits.urlRequest(baseURL: baseURL) else {
throw APIError.invalidURL
}
guard let data = try? await call(request: request) else {
throw APIError.unexpectedResponse
}
guard let fruits = getDecodedFruitesResopnse(from: data) else {
throw APIError.unexpectedResponse
}
return fruits
}
private func getDecodedFruitesResopnse(from data: Data)-> [Fruits]? {
guard let fruites = try? JSONDecoder().decode([Fruits].self, from: data) else {
return nil
}
return fruites
}
}
extension RealFruitsRepository {
enum API {
case allFruits
case fruitDetails(Fruits)
}
}
extension RealFruitsRepository.API: APICall {
var path: String {
switch self {
case .allFruits:
return "/all"
case let .fruitDetails(fruit):
let encodedName = fruit.name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
return "/name/\(encodedName ?? fruit.name)"
}
}
var method: String {
switch self {
case .allFruits, .fruitDetails:
return "GET"
}
}
var headers: [String: String]? {
return ["Accept": "application/json"]
}
func body() throws -> Data? {
return nil
}
}
Here is the model class ..
import Foundation
import Combine
protocol FruitsModelInput {
func getFruits() async
}
protocol FruitsModelOutput {
var state: FruitViewStates { get }
var fruitRecordsCount: Int { get }
func getFruit(index: Int)-> Fruits
func getFruitsDetails(for row:Int)-> FruitsDetails
}
struct FruitsDetails {
let genus, name: String
}
final class FruitsModel: ObservableObject {
private var fruitsRepository: FruitsRepository
var fruits: [Fruits] = []
#Published var state: FruitViewStates = .none
private var cancellables:Set<AnyCancellable> = Set()
init(fruitRepository: FruitsRepository) {
self.fruitsRepository = fruitRepository
}
}
extension FruitsModel: FruitsModelOutput {
func getFruitsDetails(for row: Int) -> FruitsDetails {
if row >= 0 {
let fruit = fruits[row]
return FruitsDetails(genus: fruit.genus, name: fruit.name)
}
return FruitsDetails(genus: "", name: "")
}
var fruitRecordsCount: Int {
return fruits.count
}
func getFruit(index: Int) -> Fruits {
if fruits.count > 0 {
return (fruits[index])
} else {
return Fruits(genus: "", name: "", id: 0, family: "", order: "", nutritions: Nutritions(carbohydrates: 0.0, protein: 0.0, fat: 0.0, calories: 0, sugar: 0.0))
}
}
}
extension FruitsModel: FruitsModelInput {
func getFruits() async {
state = .showActivityIndicator
do {
fruits = try await fruitsRepository.loadFruits()
self.state = .showFruitList
} catch let error {
fruits = []
print(error)
state = .showError((error as! APIError).localizedDescription)
}
}
}
Here is my async function class:
class MoviesViewModel: ObservableObject {
#Published var topRated: [Movie] = []
#Published var popular: [Movie] = []
#Published var upcoming: [Movie] = []
func getUpcomingMovies() {
if let movies = getMovies(path: "upcoming") {
DispatchQueue.main.async {
self.upcoming = movies
}
}
}
func getPopularMovies() {
if let movies = getMovies(path: "popular") {
DispatchQueue.main.async {
self.popular = movies
}
}
}
func getTopRatedMovies() {
DispatchQueue.main.async {
if let movies = self.getMovies(path: "top_rated") {
self.topRated = movies
}
}
}
func getMovies(path: String) -> [Movie]? {
var movies: [Movie]?
let urlString = "https://api.themoviedb.org/3/movie/\(path)?api_key=\(apiKey)&language=en-US&page=1"
guard let url = URL(string: urlString) else { return [] }
let session = URLSession.shared
let dataTask = session.dataTask(with: url, completionHandler: { data, _, error in
if error != nil {
print(error)
}
do {
if let safeData = data {
let decodedData = try JSONDecoder().decode(NowPlaying.self, from: safeData)
DispatchQueue.main.async {
movies = decodedData.results
}
}
}
catch {
print(error)
}
})
dataTask.resume()
return movies
}
}
When I printed the movies in getMovies function, I can get movies from api without problem. However, UI does not update itself. I used DispatchQueue.main.async function but it did not solve my problem. What can I do in this situation?
dataTask works asynchronously. Your code returns nil even before the asynchronous task is going to start. You have to use a completion handler as described in Returning data from async call in Swift function.
I highly recommend to use async/await in this case. You get rid of a lot of boilerplate code and you don't need to care about dispatching threads.
#MainActor
class MoviesViewModel: ObservableObject {
#Published var topRated: [Movie] = []
#Published var popular: [Movie] = []
#Published var upcoming: [Movie] = []
func getUpcomingMovies() async throws {
self.upcoming = try await getMovies(path: "upcoming")
}
func getPopularMovies() async throws {
self.popular = try await getMovies(path: "popular")
}
func getTopRatedMovies() async throws {
self.topRated = try await getMovies(path: "top_rated")
}
func getMovies(path: String) async throws -> [Movie] {
let urlString = "https://api.themoviedb.org/3/movie/\(path)?api_key=\(apiKey)&language=en-US&page=1"
guard let url = URL(string: urlString) else { throw URLError(.badURL) }
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(NowPlaying.self, from: data).results
}
}
Please tell me what could be the problem with this error and how to fix it?
I'm use SwiftUI 2.0
"Type 'Favorites.Type' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols"
Code:
class Favorites: ObservableObject {
private var tasks: Set<String>
let defaults = UserDefaults.standard
init() {
let decoder = JSONDecoder()
if let data = defaults.value(forKey: "Favorites") as? Data {
let taskData = try? decoder.decode(Set<String>.self, from: data)
self.tasks = taskData ?? []
} else {
self.tasks = []
}
}
func getTaskIds() -> Set<String> {
return self.tasks
}
func isEmpty() -> Bool {
tasks.count < 1
}
func contains(_ task: dataTypeFont) -> Bool {
tasks.contains(task.id)
}
func add(_ task: dataTypeFont) {
objectWillChange.send()
tasks.insert(task.id)
save()
}
func remove(_ task: dataTypeFont) {
objectWillChange.send()
tasks.remove(task.id)
save()
}
func save() {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(Favorites) {
defaults.set(encoded, forKey: "Favorites")
}
}
}
Screenshot Error:
Error
Typo.
According to the load method you have to encode tasks not the class type
func save() {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(tasks) {
defaults.set(encoded, forKey: "Favorites")
}
}
And don't use value(forKey: with UserDefaults, there is a dedicated method
if let data = defaults.data(forKey: "Favorites") {
I've got a UImage, that needs to rotate until my call to the server ends.
But my image rotates only one time and stops after that.
My code:
#IBOutlet var imgLoader: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global(qos: .userInitiated).async {
self.getSalons()
}
self.launchLoaderDesign()
NotificationCenter.default.addObserver(forName:NSNotification.Name("LoadAllShowNotification"), object: nil, queue: nil, using: notificationFinish)
}
func getSalons() -> Void {
let api:ApiSvain = ApiSvain()
api.getHeartStrokeSalon()
api.getSalonForHomePage()
}
func launchLoaderDesign() -> Void {
var posImg:Int = 0
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseIn, animations: { () -> Void in
self.imgLoader.transform = self.imgLoader.transform.rotated(by: CGFloat(M_PI_2))
})
{ (finished) -> Void in
self.launchLoaderDesign()
}
}
func getSalonForHomePage(){
let url = "MY_URL"
Alamofire.request(url, method: .get).validate().responseJSON
{ response in
if (response.error == nil)
{
let json = JSON(response.result.value!)
for (index, element) in json
{
let show:Show = Show(json: element, index: index)
StaticData.arrayOfShow.append(show)
}
NotificationCenter.default.post(name:NSNotification.Name("LoadAllShowNotification"), object: nil, userInfo: nil)
}
else
{
print(response.error!)
}
}
}
My function getSalonForHomePage sends a notification, and when I catch it I use performSegue to move to my new page.
I think my problem came from my misunderstanding of multi-threading.
Ps: I am using alamofire 4, for send request to my server.
I'm currently replacing PromiseKit with RxSwift, and need to convert my deferred promise to RxSwift.
Current implementation example in PromiseKit:
private var deferredDidLayout = Promise<()>.pending()
override func layoutSubviews() {
super.layoutSubviews()
self.deferredDidLayout.fulfill()
}
func setup() {
_ = self.didLayout().then {_ -> Void in
// Do my stuff only one time!
}
}
private func didLayout() -> Promise<()> {
return self.deferredDidLayout.promise
}
Current hack-implementation in RxSwift:
private let observableDidLayout = PublishSubject<Void>()
override func layoutSubviews() {
super.layoutSubviews()
self.observableDidLayout.onCompleted()
}
func setup() {
_ = self.observableDidLayout
.subscribe(onCompleted: { _ in
// Do my stuff only one time!
// Issue: Will be executed on every onCompleted() call
})
}
Thank you in regard!
PromiseKit: https://github.com/mxcl/PromiseKit
RxSwift: https://github.com/ReactiveX/RxSwift
I believe that 'Completable' is what you are looking for - https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Traits.md#creating-a-completable