How do you implement custom delegates in SwiftUI - ios

As an example I have a SwitUI ContentView. The one that comes when you first make the project.
import SwiftUI
struct ContentView: View {
var manager = TestManager()
var body: some View {
ZStack{
Color(.green)
.edgesIgnoringSafeArea(.all)
VStack {
Text("Test Text")
Button(action:{}) {
Text("Get number 2")
.font(.title)
.foregroundColor(.white)
.padding()
.overlay(RoundedRectangle(cornerRadius: 30)
.stroke(Color.white, lineWidth: 5))
}
}
}
}
}
I have a TestManager that will handle an Api call. I Made a delegate for the class that has two functions.
protocol TestManagerDelegate {
func didCorrectlyComplete(_ testName: TestManager, model: TestModel)
func didFailWithError(_ error: Error)
}
struct TestManager {
var delegate: TestManagerDelegate?
let urlString = "http://numbersapi.com/2/trivia?json"
func Get(){
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil{
self.delegate?.didFailWithError(error!)
return
}
if let safeData = data{
if let parsedData = self.parseJson(safeData){
self.delegate?.didCorrectlyComplete(self, model: parsedData)
}
}
}
task.resume()
}
}
func parseJson(_ jsonData: Data) -> TestModel?{
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(TestModel.self, from: jsonData)
let mes = decodedData.message
let model = TestModel(message: mes)
return model
} catch {
delegate?.didFailWithError(error)
return nil
}
}
}
This is the testModel data class. Only grabbing the text of the Json returned.
struct TestModel :Decodable{
let text: String
}
How do I connect the TestManager to the view and have the view handle the delegate like how we could do in in storyboards?

Regarding the TestModel
Decodable protocol (in your context) assumes you to create the model struct with all the properties, that you get via JSON. When requesting http://numbersapi.com/2/trivia?json you'll get something like:
{
"text": "2 is the number of stars in a binary star system (a stellar system consisting of two stars orbiting around their center of mass).",
"number": 2,
"found": true,
"type": "trivia"
}
Which means, your model should look like the following:
struct TestModel: Decodable {
let text: String
let number: Int
let found: Bool
let type: String
}
Regarding Delegates
In SwiftUI this approach is not reachable. Instead, developers need to adapt the Combine framework's features: property wrappers #ObservedObject, #Published, and ObservableObject protocol.
You want to put your logic into some struct. Bad news, that (currently) ObservableObject is AnyObject protocol (i.e. Class-Only Protocol). You'll need to rewrite your TestManager as class as:
class TestManager: ObservableObject {
// ...
}
Only then you could use it in your CurrentView using #ObservedObject property wrapper:
struct ContentView: View {
#ObservedObject var manager = TestManager()
// ...
}
Regarding the TestManager
Your logic now excludes the delegate as such, and you need to use your TestModel to pass the data to your CustomView. You could modify TestManager by adding new property with #Published property wrapper:
class TestManager: ObservableObject {
let urlString = "http://numbersapi.com/2/trivia?json"
// 1
#Published var model: TestModel?
func get(){
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { [weak self] (data, response, error) in
// 2
DispatchQueue.main.async {
if let safeData = data {
if let parsedData = self?.parseJson(safeData) {
// 3
self?.model = parsedData
}
}
}
}
task.resume()
}
}
private func parseJson(_ jsonData: Data) -> TestModel? {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(TestModel.self, from: jsonData)
return decodedData
} catch {
return nil
}
}
}
To be able to access your model "from outside", in your case the ContentView.
Use DispatchQueue.main.async{ } for async tasks, because Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
Simply use your parsed model.
Then in ContentView use your TestManager like this:
struct ContentView: View {
#ObservedObject var manager = TestManager()
var body: some View {
ZStack{
Color(.green)
.edgesIgnoringSafeArea(.all)
VStack {
Text("Trivia is: \(self.manager.model?.text ?? "Unknown")")
Button(action:{ self.manager.get() }) {
Text("Get number 2")
.font(.title)
.foregroundColor(.white)
.padding()
.overlay(RoundedRectangle(cornerRadius: 30)
.stroke(Color.white, lineWidth: 5))
}
}
}
}
}
Regarding HTTP
You use the link http://numbersapi.com/2/trivia?json which is not allowed by Apple, please, use https instead, or add the App Transport Security Settings key with Allow Arbitrary Loads parameter set to YES into your Info.Plist. But do this very carefully as the http link simply will not work.
Further steps
You could implement the error handling by yourself basing on the description above.
Full code (copy-paste and go):
import SwiftUI
struct ContentView: View {
#ObservedObject var manager = TestManager()
var body: some View {
ZStack{
Color(.green)
.edgesIgnoringSafeArea(.all)
VStack {
Text("Trivia is: \(self.manager.model?.text ?? "Unknown")")
Button(action:{ self.manager.get() }) {
Text("Get number 2")
.font(.title)
.foregroundColor(.white)
.padding()
.overlay(RoundedRectangle(cornerRadius: 30)
.stroke(Color.white, lineWidth: 5))
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class TestManager: ObservableObject {
let urlString = "http://numbersapi.com/2/trivia?json"
#Published var model: TestModel?
func get(){
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { [weak self] (data, response, error) in
DispatchQueue.main.async {
if let safeData = data {
if let parsedData = self?.parseJson(safeData) {
self?.model = parsedData
}
}
}
}
task.resume()
}
}
private func parseJson(_ jsonData: Data) -> TestModel? {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(TestModel.self, from: jsonData)
return decodedData
} catch {
return nil
}
}
}
struct TestModel: Decodable {
let text: String
let number: Int
let found: Bool
let type: String
}

Related

how to make A SwiftUI Image Gallery/Slideshow With Auto Scrolling in SwiftUI?

How can I make a slider when my Data is coming from API? I am using
this(below code) for static images work fine but whenever I try to
use API data then my code does not work.
How to Set the Marquee in this images.
This is My code
public struct MagazineModel: Decodable {
public let magzineBanners: [MagzineBanner]
}
public struct MagzineBanner: Decodable, Identifiable {
public let id: Int
public let url: String
}
This is My View Model
//View Model for Magazines and showing Details
class MagazineBannerVM: ObservableObject{
#Published var datas = [MagzineBanner]()
let url = "ApiUrl"
init() {
getData(url: url)
}
func getData(url: String) {
guard let url = URL(string: url) else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
if let data = data {
do {
let results = try JSONDecoder().decode(MagazineModel.self, from: data)
DispatchQueue.main.async {
self.datas = results.magzineBanners
}
}
catch {
print(error)
}
}
}.resume()
}
}
struct MagazineBannerView: View{
#ObservedObject var list = MagazineBannerVM()
public let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
#State var currentIndex = 0
#State var totalImages = 2
var body: some View{
ScrollView(.horizontal) {
GeometryReader { proxy in
TabView(selection: $currentIndex) {
HStack{
ForEach(list.datas, id: \.id){ item in
Group{
AsyncImage(url: URL(string: item.url)){ image in
image
.resizable()
.frame(width:UIScreen.main.bounds.width, height: 122)
}placeholder: {
Image("logo_gray").resizable()
.frame(width:UIScreen.main.bounds.width, height: 122)
}
}
}
}
}
.tabViewStyle(PageTabViewStyle())
.onReceive(timer, perform: { _ in
withAnimation{
currentIndex = currentIndex < totalImages ? currentIndex + 1: 0
}
})
}
}
}
}
I want to change images after every 2 seconds and every images has
full width as the screen width
And it is showing the half of screen width and showing both images in
single view

Show API Data in a View without ID

I have a small weather project and I got stuck in the phase where I have to show the results from the API in a view. The API is from WeatherAPI.
I mention that the JSON file doesn't have an id and I receive the results in Console.
What is the best approach for me to solve this problem?
Thank you for your help!
This is the APIService.
import Foundation
class APIService: ObservableObject {
func apiCall(searchedCity: String) {
let apikey = "secret"
guard let url = URL(string: "https://api.weatherapi.com/v1/current.json?key=\(apikey)&q=\(searchedCity)&aqi=no") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-type")
let task = URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data, error == nil else {
return
}
do {
let response = try JSONDecoder().decode(WeatherModel.self, from: data)
print(response)
print("SUCCESS")
}
catch {
print(error)
}
}
task.resume()
}
}
This is the ForecastModel
import Foundation
struct WeatherModel: Codable {
var location: Location
var current: Current
}
struct Location: Codable {
var name: String
}
struct Current: Codable, {
var temp_c: Decimal
var wind_kph: Decimal
}
Here is the view where I want to call.
import SwiftUI
struct WeatherView: View {
#StateObject var callApi = APIService()
#Binding var searchedCity: String
var body: some View {
NavigationView {
ZStack(alignment: .leading) {
Color.backgroundColor.ignoresSafeArea(edges: .all)
VStack(alignment: .leading, spacing: 50) {
comparation
temperature
clothes
Spacer()
}
.foregroundColor(.white)
.font(.largeTitle)
.onAppear(perform: {
self.callApi.apiCall(searchedCity: "\(searchedCity)")
})
}
}
}
}
struct WeatherView_Previews: PreviewProvider {
#State static var searchedCity: String = ""
static var previews: some View {
WeatherView(searchedCity: $searchedCity)
}
}
fileprivate extension WeatherView {
var comparation: some View {
Text("Today is ")
.fontWeight(.medium)
}
var temperature: some View {
Text("It is ?C with ? and ? winds")
.fontWeight(.medium)
}
var clothes: some View {
Text("Wear a ?")
.fontWeight(.medium)
}
}

How to navigate between two API parameters in SwiftUI

I'm beginner of iOS app development, currently doing iOS & Swift Bootcamp on Udemy by Angela Yu. I have this app called H4X0R News, which shows Hacker News all stories that are on the front/home page on the app by using its API. By the end of a module the app works fine when url property from API json is not nil but there are certain cases when url equals nil. These are posts which instead has story_text property. So what I want here to adjust is add story_text to my code and use it to navigate between this and url parameter. Here's the code I've got:
ContentView.swift
import SwiftUI
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView {
List(networkManager.posts) { post in
NavigationLink(destination: DetailView(url: post.url)) {
HStack {
Text(String(post.points))
Text(post.title)
}
}
}
.navigationTitle("H4X0R NEWS")
}
.onAppear {
self.networkManager.fechData()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
WebView.swift
import Foundation
import WebKit
import SwiftUI
struct WebView: UIViewRepresentable {
let urlString: String?
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
if let safeString = urlString {
if let url = URL(string: safeString) {
let request = URLRequest(url: url)
uiView.load(request)
}
}
}
}
DetailView.swift
import SwiftUI
struct DetailView: View {
let url: String?
var body: some View {
WebView(urlString: url)
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(url: "https://www.google.com/")
}
}
NetworkManager.swift
import Foundation
class NetworkManager: ObservableObject {
#Published var posts = [Post]()
func fechData() {
if let url = URL(string: "http://hn.algolia.com/api/v1/search?tags=front_page") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.hits
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
PostData.swift
import Foundation
struct Results: Decodable {
let hits: [Post]
}
struct Post: Decodable, Identifiable {
var id: String {
return objectID
}
let objectID: String
let points: Int
let title: String
let url: String?
}
So what I'm sure I need to add this story_text to PostData as String? and then make conditional statement in the WebView.updateUIView() function. Then update the code in other files. But like I said, I'm new in the programming world and I seek for help here for the first time since I've started the course.
If I understand you correct, you want to navigate to a simple text view if there is no URL for the Post?
So you can do it like this:
let text: String?
var body: some View {
if let url = url {
WebView(urlString: url)
} else {
Text(text ?? "-")
}
}

Why is my #AppStorage not working on SwiftUI?

I'm trying to set up the #AppStorage wrapper in my project.
I'm pulling Texts from a JSON API (see DataModel), and am hoping to store the results in UserDefautls. I want the data to be fetched .OnAppear and stored into the #AppStorage. When the user taps "Get Next Text", I want a new poem to be fetched, and to update #AppStorage with the newest Text data, (which would delete the past Poem stored).
Currently, the code below builds but does not display anything in the Text(currentPoemTitle).
Data Model
import Foundation
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
public class FetchPoem: ObservableObject {
// 1.
#Published var poems = [Poem]()
init() {
getPoem()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let poemData = data {
// 3.
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
TestView
import SwiftUI
struct Test: View {
#ObservedObject var fetch = FetchPoem()
#AppStorage("currentPoemtTitle") var currentPoemTitle = ""
#AppStorage("currentPoemAuthor") var currentPoemAuthor = ""
var body: some View {
VStack{
Text(currentPoemTitle)
Button("Fetch next text") {
fetch.getPoem()
}
}.onAppear{
if let poem = fetch.poems.first {
currentPoemTitle = "\(poem.title)"
currentPoemAuthor = "\(poem.author)"
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
What am I missing? Thanks.
Here are a few code edits to get you going.
I added AppStorageKeys to manage the #AppStorage keys, to avoid errors retyping key strings (ie. "currentPoemtTitle")
Your question asked how to update the #AppStorage with the data, and the simple solution is to add the #AppStorage variables within the FetchPoem class and set them within the FetchPoem class after the data is downloaded. This also avoids the need for the .onAppear function.
The purpose of using #ObservedObject is to be able to keep your View in sync with the data. By adding the extra layer of #AppStorage, you make the #ObservedObject sort of pointless. Within the View, I added a Text() to display the title using the #ObservedObject values directly, instead of relying on #AppStorage. I'm not sure if you want this, but it would remove the need for the #AppStorage variables entirely.
I also added a getPoems2() function using Combine, which is a new framework from Apple to download async data. It makes the code a little easier/more efficient... getPoems() and getPoems2() both work and do the same thing :)
Code:
import Foundation
import SwiftUI
import Combine
struct AppStorageKeys {
static let poemTitle = "current_poem_title"
static let poemAuthor = "current_poem_author"
}
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
public class FetchPoem: ObservableObject {
#Published var poems = [Poem]()
#AppStorage(AppStorageKeys.poemTitle) var poemTitle = ""
#AppStorage(AppStorageKeys.poemAuthor) var poemAuthor = ""
init() {
getPoem2()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
guard let poemData = data else {
print("No data")
return
}
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
self.updateFirstPoem()
}
} catch {
print("Error")
}
}
.resume()
}
func getPoem2() {
let url = URL(string: "https://poetrydb.org/random/1")!
URLSession.shared.dataTaskPublisher(for: url)
// fetch on background thread
.subscribe(on: DispatchQueue.global(qos: .background))
// recieve response on main thread
.receive(on: DispatchQueue.main)
// ensure there is data
.tryMap { (data, response) in
guard
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
// decode JSON data to [Poem]
.decode(type: [Poem].self, decoder: JSONDecoder())
// Handle results
.sink { (result) in
// will return success or failure
print("poetry fetch completion: \(result)")
} receiveValue: { (value) in
// if success, will return [Poem]
// here you can update your view
self.poems = value
self.updateFirstPoem()
}
// After recieving response, the URLSession is no longer needed & we can cancel the publisher
.cancel()
}
func updateFirstPoem() {
if let firstPoem = self.poems.first {
self.poemTitle = firstPoem.title
self.poemAuthor = firstPoem.author
}
}
}
struct Test: View {
#ObservedObject var fetch = FetchPoem()
#AppStorage(AppStorageKeys.poemTitle) var currentPoemTitle = ""
#AppStorage(AppStorageKeys.poemAuthor) var currentPoemAuthor = ""
var body: some View {
VStack(spacing: 10){
Text("App Storage:")
Text(currentPoemTitle)
Text(currentPoemAuthor)
Divider()
Text("Observed Object:")
Text(fetch.poems.first?.title ?? "")
Text(fetch.poems.first?.author ?? "")
Button("Fetch next text") {
fetch.getPoem()
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}

Cannot fetch data from Cloud Firestore in SwiftUI Button view

Why does this not work? There is data in Firestore, but it appears the block doesn't execute:
import SwiftUI
import FirebaseFirestore
struct ContentView: View {
var body: some View {
VStack{
Button(
action: {
print("Getting data...")
let db = Firestore.firestore().collection("menu")
let query = db.order(by: "name", descending: true)
query.getDocuments() { snapshot, err in
guard let snapshot = snapshot else {
return
}
print(snapshot)
for doc in snapshot.documents {
print(doc)
}
}
},
label: { Text("Click Me") }
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You shouldn't add data access logic to a button's action handler. Instead, extract data access logic into a view model (or even a repository), and then add a subscription on the view model's properties like this:
Hypothetical menu item data model:
struct MenuItem: Identifiable {
var id: String = UUID().uuidString
var title: String
}
View Model:
import Foundation
import FirebaseFirestore
class MenuItemsViewModel: ObservableObject {
#Published var menuItems = [MenuItem]()
private var db = Firestore.firestore()
func fetchData() {
db.collection("menuitems").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.menuItems = documents.map { queryDocumentSnapshot -> MenuItem in
let data = queryDocumentSnapshot.data()
let title = data["title"] as? String ?? ""
return MenuItem(id: .init(), title: title)
}
}
}
}
And in your view:
struct MenuItemsListView: View {
#ObservedObject var viewModel = MenuItemsViewModel()
var body: some View {
NavigationView {
List(viewModel.menuItems) { menuItem in
VStack(alignment: .leading) {
Text(menuItem.title)
.font(.headline)
}
}
.navigationBarTitle("Menu")
.onAppear() { // (3)
self.viewModel.fetchData()
}
}
}
}
Data mapping can be further simplified by using Firestore's Codable support:
Here's how you need to update the model:
import FirebaseFirestoreSwift
struct MenuItem: Identifiable, Codable {
#DocumentID var id: String? = UUID().uuidString
var title: String
}
And here is the updated fetchData() method:
func fetchData() {
db.collection("menuitems").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.menuItems = documents.compactMap { (queryDocumentSnapshot) -> MenuItem? in
return try? queryDocumentSnapshot.data(as: MenuItem.self)
}
}
}
If this doesn't work, check the log for any error messages (your security rules might be preventing you from reading data, for example).

Resources