Widget element disappears in Widget Gallery - Swift - ios

I have a weird issue I'm not sure how to resolve. I have a weather app that displays Celsius and Fahrenheit at the same time, as well as an SF symbol for the weather condition.
Everything in the widget works perfectly on it's own. However, in the widget gallery, the SF symbol will disappear from the widget after about half a second and I have no idea why.
Here's all the code from the widget. It's my first attempt at one, so it's not very clean:
import WidgetKit
import SwiftUI
let otherUserDefaults = UserDefaults(suiteName: "group.DualTemp")
let temperatureCelsius = otherUserDefaults?.value(forKey: "CTempWidget") ?? 0
let temperatureFahrenheit = otherUserDefaults?.value(forKey: "FTempWidget")
let name = otherUserDefaults?.value(forKey: "SearchedCityWidget")
let weatherID = otherUserDefaults?.string(forKey: "WidgetIcon")
let weather = UIImage(systemName: weatherID!)
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), name: "", temperatureCelsius: "0°C", temperatureFahrenheit: "0°F", weatherName: "Cloud", weatherIcon: weather!)
}
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), name: name as! String , temperatureCelsius: temperatureCelsius as! String, temperatureFahrenheit: temperatureFahrenheit as! String, weatherName: "sun", weatherIcon: weather!)
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
let userDefaults = UserDefaults(suiteName: "group.DualTemp")
let temperatureCelsius = userDefaults?.value(forKey: "CTempWidget") ?? 0
let temperatureFahrenheit = userDefaults?.value(forKey: "FTempWidget")
let name = userDefaults?.value(forKey: "SearchedCityWidget")
let weatherName = userDefaults?.string(forKey: "WidgetIcon")
let weatherImage = UIImage(systemName: weatherName!)
//MARK: - refresh info
let totalCountdown = 30
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for offset in 0 ..< totalCountdown {
let entryDate = Calendar.current.date(byAdding: .minute, value: offset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, name: name as? String ?? "No location", temperatureCelsius: temperatureCelsius as? String ?? "0°C", temperatureFahrenheit: temperatureFahrenheit as? String ?? "0°F", weatherName: weatherName!,
weatherIcon: weatherImage!)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let name: String
let temperatureCelsius: String
let temperatureFahrenheit: String
let weatherName: String
let weatherIcon: UIImage
}
struct DualTempWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
ZStack{
Color("WidgetBackground")
VStack(spacing: 0.5) {
if #available(iOSApplicationExtension 16.0, *) {
Text(entry.name)
.padding(6)
.bold()
.font(.system(size: 27))
.minimumScaleFactor(0.01)
.lineLimit(1)
} else {
Text(entry.name)
.padding(6)
.font(.system(size: 29))
.minimumScaleFactor(0.01)
.lineLimit(1)
// Fallback on earlier versions
}
Image(systemName: entry.weatherName)
.resizable()
.symbolRenderingMode(.palette)
.scaledToFit()
.frame(width: 70, height: 70)
HStack{
Text(entry.temperatureCelsius)
.font(.system(size: 22))
Text("|")
Text(entry.temperatureFahrenheit)
.font(.system(size: 22))
}
}
}
}
}
struct DualTempWidget: Widget {
let kind: String = "DualTempWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
DualTempWidgetEntryView(entry: entry)
}
.configurationDisplayName("Dual Temp")
.description("See the current temperature, in celsius and fahrenheit, and weather conditions for your location.")
.supportedFamilies([.systemSmall])
}
}
struct DualTempWidget_Previews: PreviewProvider {
static var previews: some View {
DualTempWidgetEntryView(entry: SimpleEntry(date: Date(), name: name as! String , temperatureCelsius: temperatureCelsius as! String, temperatureFahrenheit: temperatureFahrenheit as! String, weatherName: weatherID!, weatherIcon: weather!))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
Everything looks as it should be as everything in the widget updates correctly when you search for a new place in the app itself. Weird.

Related

Implementing In App Purchases in IOS Widget

I'm currently stuck trying to implement in app purchases within my IOS Widget. My widget works, the in-app purchase works, and the app works for the baseline functions.
I am unable to make the purchases available within the widget. My current strategy is to use if else statements to turn change with parts of code to point to.
Could anyone tell me how to implement in app purchases within my Widget?
Here is my code:
code
import WidgetKit
import SwiftUI
struct Provider: TimelineProvider {
let userDefaults = UserDefaults(suiteName: "group.QuotePackage")
func placeholder(in context: Context) -> SimpleEntry {
_ = Quote.self
if UserDefaults.standard.bool(forKey: "premiumQuotes") == true {
// remove adds
return SimpleEntry(date: Date(), quoteDetails: QuoteProvider.random())
} else {
return SimpleEntry(date: Date(), quoteDetails: QuoteProvider2.random2())
}
}
//let userDefaults = UserDefaults(suiteName: "group.QuotePackage")
public typealias Entry = SimpleEntry
public func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> ()) {
if UserDefaults.standard.bool(forKey: "premiumQuotes") == true {
// remove adds
let entry = SimpleEntry(date: Date(), quoteDetails: QuoteProvider.random())
} else {
let entry = SimpleEntry(date: Date(), quoteDetails: QuoteProvider2.random2())
}
completion(entry)
}
public func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> Void) {
var entries: [SimpleEntry] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
if UserDefaults.standard.bool(forKey: "premiumQuotes") == true {
// remove adds
let entry = SimpleEntry(date: entryDate, quoteDetails: QuoteProvider.random())
} else {
let entry = SimpleEntry(date: entryDate, quoteDetails: QuoteProvider2.random2())
}
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct Quote_WidgetEntryView: View {
var entry: Provider.Entry
var body: some View {
QuoteWidgetEntryView(quoteDetails: entry.quoteDetails)
}
}
struct SimpleEntry: TimelineEntry {
public let date: Date
public let quoteDetails: QuoteDetails
}
struct PlaceholderView : View {
var body: some View {
Text("Inspo")
}
}
struct QuoteWidgetEntryView : View {
let quoteDetails: QuoteDetails
var body: some View {
ZStack {
Image("background")
.resizable()
.aspectRatio(contentMode: .fill)
VStack (alignment: .center, spacing: 15) {
Text("\"\(quoteDetails.texts)\"")
.font(Font.custom("Georgia", size: 20))
.fontWeight(.light)
.foregroundColor(Color.white)
.multilineTextAlignment(.center)
.lineLimit(5)
.padding(.leading, 20)
.padding(.trailing, 20)
Text("\(quoteDetails.authors)")
.font(Font.custom("Georgia", size: 15))
.fontWeight(.light)
.foregroundColor(Color.white)
}
}
}
}
#main
struct Quote2_Widget: Widget {
private let kind: String = "Quote2_Widget"
public var body: some WidgetConfiguration {
if UserDefaults.standard.bool(forKey: "premiumQuotes") == true {
// remove adds
StaticConfiguration(kind: kind, provider: Provider()) { entry in
QuoteWidgetEntryView(quoteDetails: QuoteProvider.random())
} else {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
QuoteWidgetEntryView(quoteDetails: QuoteProvider.random())
}
}
.configurationDisplayName("Inspo")
.description("Add Inspo Quotes to your homescreen.")
.supportedFamilies([.systemMedium])
}
}
}
struct QuoteWidgetView_Previews: PreviewProvider {
static var previews: some View {
QuoteWidgetEntryView(quoteDetails: QuoteProvider.random())
.previewContext(WidgetPreviewContext(family: .systemMedium))
}
}

Error while running widgetkit extension target on physical device

Added a WidgetBuilder to my extension so I could include a second type of widget for my app. I keep getting this error in a popup when building it onto my device.
Details
SendProcessControlEvent:toPid: encountered an error: Error Domain=com.apple.dt.deviceprocesscontrolservice Code=8 "Failed to show Widget 'com.identifier' error: Error Domain=SBAvocadoDebuggingControllerErrorDomain Code=2 "Please specify the widget kind in the scheme's Environment Variables using the key '_XCWidgetKind' to be one of: 'QuoteWidget', 'RandomWidget'" UserInfo={NSLocalizedDescription=Please specify the widget kind in the scheme's Environment Variables using the key '_XCWidgetKind' to be one of: 'QuoteWidget', 'RandomWidget'}." UserInfo={NSLocalizedDescription=Failed to show Widget 'com.identifiert' error: Error Domain=SBAvocadoDebuggingControllerErrorDomain Code=2 "Please specify the widget kind in the scheme's Environment Variables using the key '_XCWidgetKind' to be one of: 'QuoteWidget', 'RandomWidget'" UserInfo={NSLocalizedDescription=Please specify the widget kind in the scheme's Environment Variables using the key '_XCWidgetKind' to be one of: 'QuoteWidget', 'RandomWidget'}., NSUnderlyingError=0x12f915290 {Error Domain=SBAvocadoDebuggingControllerErrorDomain Code=2 "Please specify the widget kind in the scheme's Environment Variables using the key '_XCWidgetKind' to be one of: 'QuoteWidget', 'RandomWidget'" UserInfo={NSLocalizedDescription=Please specify the widget kind in the scheme's Environment Variables using the key '_XCWidgetKind' to be one of: 'QuoteWidget', 'RandomWidget'}}}
Domain: DTXMessage
Code: 1
--
Heres the before and after code.
Before adding the second widget:
import WidgetKit
import SwiftUI
import Foundation
let testBook = Book(id: UUID(), name: "Name", author: "Author", genre: "Error", page: "0", total: "77")
public enum AppGroup: String {
case Livre = "group.com.idetifier"
public var containerURL: URL {
switch self {
case .Livre:
return FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: self.rawValue)!
}
}
}
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), book: testBook)
}
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), book: testBook)
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let currentDate = Date()
let entryDate = Calendar.current.date(byAdding: .minute, value: 30, to: currentDate)!
let books: [Book] = self.load("list")
let entry = SimpleEntry(date: currentDate, book: books.randomElement() ?? testBook)
let timeline = Timeline(entries: [entry], policy: .after(entryDate))
completion(timeline)
}
func load<T: Decodable>(_ filename: String) -> T {
let groupDirectory = AppGroup.Livre.containerURL
let groupURL = groupDirectory
.appendingPathComponent(filename)
.appendingPathExtension("json")
return try! JSONDecoder().decode(T.self, from: Data(contentsOf: groupURL))
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let book: Book
}
struct PlaceholderView: View {
var body: some View {
RandomWidgetView(book: testBook)
.redacted(reason: .placeholder)
}
}
struct LivreWidgetEntryView : View {
#Environment(\.widgetFamily) var family
var entry: Provider.Entry
#ViewBuilder
var body: some View {
switch family {
case .systemSmall:
RandomWidgetView(book: entry.book, size: .small)
case .systemMedium:
RandomWidgetView(book: entry.book, size: .medium)
case .systemLarge:
RandomWidgetView(book: entry.book, size: .large)
default:
RandomWidgetView(book: entry.book, size: .small)
}
}
}
#main
struct LivreWidget: Widget {
let kind: String = "LivreWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
LivreWidgetEntryView(entry: entry)
}
.configurationDisplayName("Progress of a random book")
.description("This widget shows the progress from a random book in your Library")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
and after adding the second widget:
import WidgetKit
import SwiftUI
import Foundation
#main
struct LivreWidgetBuilder: WidgetBundle {
init() {}
#WidgetBundleBuilder
var body: some Widget {
RandomWidget()
QuoteWidget()
}
}
let testBook = Book(id: UUID(), name: "Name", author: "Author", genre: "Error", page: "0", total: "77", quotes: ["Quote"])
public enum AppGroup: String {
case Livre = "group.com.identifier"
public var containerURL: URL {
switch self {
case .Livre:
return FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: self.rawValue)!
}
}
}
//MARK: Random book widget
struct RandomProvider: TimelineProvider {
func placeholder(in context: Context) -> RandomSimpleEntry {
RandomSimpleEntry(date: Date(), book: testBook)
}
func getSnapshot(in context: Context, completion: #escaping (RandomSimpleEntry) -> ()) {
let entry = RandomSimpleEntry(date: Date(), book: testBook)
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let currentDate = Date()
let entryDate = Calendar.current.date(byAdding: .minute, value: 30, to: currentDate)!
let books: [Book] = self.load("list")
let entry = RandomSimpleEntry(date: currentDate, book: books.randomElement() ?? testBook)
let timeline = Timeline(entries: [entry], policy: .after(entryDate))
completion(timeline)
}
func load<T: Decodable>(_ filename: String) -> T {
let groupDirectory = AppGroup.Livre.containerURL
let groupURL = groupDirectory
.appendingPathComponent(filename)
.appendingPathExtension("json")
return try! JSONDecoder().decode(T.self, from: Data(contentsOf: groupURL))
}
}
struct RandomSimpleEntry: TimelineEntry {
let date: Date
let book: Book
}
struct RandomPlaceholderView: View {
var body: some View {
RandomWidgetView(book: testBook)
.redacted(reason: .placeholder)
}
}
struct RandomWidgetEntryView : View {
#Environment(\.widgetFamily) var family
var entry: RandomProvider.Entry
#ViewBuilder
var body: some View {
switch family {
case .systemSmall:
RandomWidgetView(book: entry.book, size: .small)
case .systemMedium:
RandomWidgetView(book: entry.book, size: .medium)
case .systemLarge:
RandomWidgetView(book: entry.book, size: .large)
default:
RandomWidgetView(book: entry.book, size: .small)
}
}
}
struct RandomWidget: Widget {
let kind: String = "RandomWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: RandomProvider()) { entry in
RandomWidgetEntryView(entry: entry)
}
.configurationDisplayName("Random book progress")
.description("This widget shows the progress from a random book in your Library")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
//MARK: Quote Widget
struct QuoteProvider: TimelineProvider {
func placeholder(in context: Context) -> QuoteSimpleEntry {
QuoteSimpleEntry(date: Date(), book: testBook)
}
func getSnapshot(in context: Context, completion: #escaping (QuoteSimpleEntry) -> ()) {
let entry = QuoteSimpleEntry(date: Date(), book: testBook)
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let currentDate = Date()
let entryDate = Calendar.current.date(byAdding: .minute, value: 30, to: currentDate)!
let books: [Book] = self.load("list")
let entry = QuoteSimpleEntry(date: currentDate, book: books.randomElement() ?? testBook)
let timeline = Timeline(entries: [entry], policy: .after(entryDate))
completion(timeline)
}
func load<T: Decodable>(_ filename: String) -> T {
let groupDirectory = AppGroup.Livre.containerURL
let groupURL = groupDirectory
.appendingPathComponent(filename)
.appendingPathExtension("json")
return try! JSONDecoder().decode(T.self, from: Data(contentsOf: groupURL))
}
}
struct QuoteSimpleEntry: TimelineEntry {
let date: Date
let book: Book
}
struct QuotePlaceholderView: View {
var body: some View {
QuoteWidgetView(book: testBook)
.redacted(reason: .placeholder)
}
}
struct QuoteWidgetEntryView : View {
#Environment(\.widgetFamily) var family
var entry: QuoteProvider.Entry
var body: some View {
QuoteWidgetView(book: entry.book, size: .small)
}
}
struct QuoteWidget: Widget {
let kind: String = "QuoteWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: QuoteProvider()) { entry in
QuoteWidgetEntryView(entry: entry)
}
.configurationDisplayName("Quote widget")
.description("This widget shows a random quote from a random book in your Library")
.supportedFamilies([.systemMedium])
}
}
The widgets seem to work, but I'm trying to find out why i'm getting this popup error.

Pass data entered in a UIView to a Widget

I try to implement a simple pattern on Widgets:
get a textField in the app;
have the Widget updated when textField is changed.
App is UIKit (not SwiftUI).
I've read here that I could pass it through UserDefaults,
How to pass uiviewcontroller data to swiftui widget class
and also through a shared singleton. I tried but couldn't make it work.
What I tried:
Create a singleton to hold the data to pass:
class Util {
class var shared : Util {
struct Singleton {
static let instance = Util()
}
return Singleton.instance;
}
var globalToPass = "Hello"
}
shared the file between the 2 targets App and WidgetExtension
In VC, update the singleton when textField is changed and ask for widget to reload timeline
#IBAction func updateMessage(_ sender: UITextField) {
Util.shared.globalToPass = valueToPassLabel.text ?? "--"
WidgetCenter.shared.reloadTimelines(ofKind: "WidgetForTest")
WidgetCenter.shared.reloadAllTimelines()
}
Problem : Widget never updated its message field
Here is the full widget code at this time:
import WidgetKit
import SwiftUI
struct LoadStatusProvider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), loadEntry: 0, message: Util.shared.globalToPass)
}
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), loadEntry: 0, message: Util.shared.globalToPass)
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for minuteOffset in 0 ..< 2 {
let entryDate = Calendar.current.date(byAdding: .minute, value: 5*minuteOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, loadEntry: minuteOffset, message: Util.shared.globalToPass)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let loadEntry: Int
let message: String
}
struct WidgetForTestNoIntentEntryView : View {
var entry: LoadStatusProvider.Entry
var body: some View {
let formatter = DateFormatter()
formatter.timeStyle = .medium
let dateString = formatter.string(from: entry.date)
return
VStack {
Text(String(entry.message))
HStack {
Text("Started")
Text(entry.date, style: .time)
}
HStack {
Text("Now")
Text(dateString)
}
HStack {
Text("Loaded")
Text(String(entry.loadEntry))
}
}
}
}
#main
struct WidgetForTestNoIntent: Widget {
let kind: String = "WidgetForTestNoIntent"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: LoadStatusProvider()) { entry in
WidgetForTestNoIntentEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
struct WidgetForTestNoIntent_Previews: PreviewProvider {
static var previews: some View {
WidgetForTestNoIntentEntryView(entry: SimpleEntry(date: Date(), loadEntry: 0, message: "-"))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}

RSS Widget For iOS 14 Shows Empty

I use an XMLParser for my app written in Swift, and am trying to get a Widget set up for iOS 14 in which it will show the most recent article on the RSS. I see the placeholder just fine when I add it, but it stays empty with just a line where text should be and never seems to get the information from the timeline.
struct Provider: TimelineProvider {
#State private var rssItems:[RSSItem]?
let feedParser = FeedParser()
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), title:"News", description: "News article here", link: "Http://link", pubDate: "The day it posted")
}
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), title:"News", description: "News Article Here", link: "Http://link", pubDate: "The day it posted")
completion(entry)
}
func getTimeline(in context: Context, completion: #escaping (Timeline<SimpleEntry>) -> ()) {
var entries: [SimpleEntry] = []
feedParser.parseFeed(url: "http://fritchcoc.wordpress.com/feed") {(rssItems) in
self.rssItems = rssItems
let currentDate = Date()
let entry = SimpleEntry(date: currentDate, title:rssItems[0].title, description: rssItems[0].description, link: rssItems[0].link, pubDate: rssItems[0].pubDate)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let title: String
let description: String
let link: String
let pubDate: String
}
struct FritchNewsEntryView : View {
var entry: SimpleEntry
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(entry.title)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .leading)
.padding()
.background(LinearGradient(gradient: Gradient(colors: [.orange, .yellow]), startPoint: .top, endPoint: .bottom))
}
}
#main
struct FritchNews: Widget {
let kind: String = "FritchNews"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
FritchNewsEntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
UPDATE BY THE OP:
It was indeed a combination of the timeline being set in the wrong spot, along with a couple of typos within the parser that caused this particular issue.
Apple does not allow HTTP connections by default, the easiest solution is to change the URL to https://fritchcoc.wordpress.com/feed
If you want to allow HTTP connections in your app (or widget) you can add exceptions (to one domain or all) in the Info.plist for the target, see NSAppTransportSecurity at Apple Docs
Edit:
After a better look and finding for the implementation of FeedParser online, I noticed that parseFeed() is asynchronous.
Thus the completion is called with an empty array, it should be called after the parsing is done:
struct Provider: TimelineProvider {
...
func getTimeline(in context: Context, completion: #escaping (Timeline<SimpleEntry>) -> ()) {
var entries: [SimpleEntry] = []
feedParser.parseFeed(url: "http://fritchcoc.wordpress.com/feed") {(rssItems) in
self.rssItems = rssItems
let currentDate = Date()
let entry = SimpleEntry(date: currentDate, title:rssItems[0].title, description: rssItems[0].description, link: rssItems[0].link, pubDate: rssItems[0].pubDate)
entries.append(entry)
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
}

Issues Switching From UIKit to SwiftUI and Passing API Data to the View

I just converted my app to the SwiftUI framework. The front end works great, but I am having a huge issue - I can't figure out how to fetch my data from a user UITextField entry and update my view. This project has a lot of code, but I've condensed it into a few files and will share only a portion since the other portions of the app are structured in a similar manner. I would really appreciate help anyone can offer. I intend to replace many of the strings in the METARView Text labels with variables based on the decoded API call data.
Front End: ContentView()
//
// ContentView.swift
// Aviate
//
// Created by Grayson Bertaina on 9/27/20.
//
import Combine
import SwiftUI
import Foundation
class Query: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
}
struct ContentView: View {
init() {
UITableView.appearance().backgroundColor = .clear
}
#State var stationQuery: String = ""
var body: some View {
NavigationView{
VStack {
Text("The App for GA Pilots")
Spacer()
VStack(alignment: .leading){
Text("News")
.background(Color.blue)
.font(.title)
.padding()
Text("Aviate now supports chart lookup functionality!")
.padding()
}
.background(Color.blue)
.foregroundColor(.white)
.padding()
VStack(alignment: .leading) {
Text("Favorite Airports")
.font(.title)
.padding()
List {
Section(header: Text("Favorite Airports")) {
NavigationLink(destination: METARView(search: stationQuery)) {
Text("Airport 1")
}
}
Section(header: Text("Nearby Stations")) {
NavigationLink(destination: METARView(search: stationQuery)) {
Text("Airport 1")
}
}
}
}
Spacer()
HStack{
TextField("Enter ICAO", text: $stationQuery)
.padding()
.border(Color.black)
NavigationLink(destination: METARView(search: stationQuery)) {
Text("Search")
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
}
.padding()
}
.navigationBarTitle("Aviate Home")
}
}
}
struct METARView: View {
#State var weatherManager = WeatherManager()
init(search: String) {
self.weatherManager.fetchWeather(stationICAO: search)
}
var body: some View {
ScrollView{
METARReport(model: weatherManager.weather)
MapView()
.frame(height: 200)
.edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
METARTitleView()
.frame(height: 250)
METARReport()
Spacer()
}
.edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
}
}
struct TAFView: View {
var search: String
var body: some View {
ScrollView{
MapView()
.frame(height: 200)
.edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
METARTitleView()
.frame(height: 250)
TAFReport()
}
.edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
}
}
struct AirportView: View {
var search: String
var body: some View {
ScrollView{
MapView()
.frame(height: 200)
.edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
AirportReport()
}
.edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewDevice("iPhone 11")
}
}
final class DataModel: ObservableObject {
static let shared = DataModel()
#Published var observedString = "" {
didSet {
//RUNS SUCCESSFULLY
print("Observed string changed to \(observedString)")
}
}
}
Here is the part of METARView() that is needed to understand where the data is going - it will present the pulled data.
METARReport()
//
// METARReport.swift
// Aviate
//
// Created by Grayson Bertaina on 9/29/20.
//
import SwiftUI
import Foundation
var weatherManager = WeatherManager()
struct METARReport: View {
var body: some View {
ScrollView {
VStack{
HStack{
Text("METAR Report")
.font(.title)
Spacer()
Text("Time: " + "")
.font(.headline)
}
.padding()
HStack{
Text("Flight Rules")
Spacer()
Text("VFR")
.padding()
.background(Color.green)
}
.padding()
.font(.headline)
HStack(spacing: 20){
VStack(alignment: .leading){
Text("Visibility")
Spacer()
Text("10" + " SM")
}
VStack(alignment: .leading){
Text("Temperature")
Spacer()
Text("25" + " °C")
}
VStack(alignment: .leading){
Text("Dew Point")
Spacer()
Text("25" + " °C")
}
}
.padding()
.font(.headline)
VStack(alignment: .center){
Text("Clouds")
.font(.headline)
.padding()
HStack() {
Text("Few")
Spacer()
Text("1200")
}
.padding()
HStack() {
Text("Few")
Spacer()
Text("1200")
}
.padding()
HStack() {
Text("Few")
Spacer()
Text("1200")
}
.padding()
}
.font(.headline)
HStack{
Text("Altimeter")
Spacer()
Text("29.92")
}
.font(.headline)
.padding()
VStack{
Text("Wind")
HStack{
Text("5" + "kt")
Text("G")
Text("12" + "kt")
Text("From")
Text("310" + "°")
}
.padding()
}
.font(.headline)
HStack {
Text("Special Wx")
Spacer()
Text("+RA")
}
.padding()
.font(.headline)
VStack {
Text("Remarks")
Spacer()
Text("No Remarks")
}
.padding()
.font(.headline)
}
}
}
}
struct METARReport_Previews: PreviewProvider {
static var previews: some View {
METARReport()
}
}
Back End: WeatherStorage (Contains WeatherData, WeatherModel, and WeatherManager with API Call)
struct WeatherData: Codable {
let flight_rules: String?
let remarks: String?
let wind_speed: WindSpeed?
let wind_gust: WindGust?
let wind_direction: WindDirection?
let visibility: Visibility?
let time: Time?
let station: String?
let temperature: Temperature?
let raw: String?
let clouds: [Clouds?]
let wx_codes: [Wxcodes?]
}
struct Clouds: Codable {
let type: String
let altitude: Int
}
struct Time: Codable {
let repr: String
}
struct WindSpeed: Codable {
let value: Int
}
struct WindGust: Codable {
let value: Int
}
struct WindDirection: Codable {
let repr: String
}
struct Visibility: Codable {
let repr: String
}
struct Remarks: Codable {
let remarks: String
}
struct Altimeter: Codable {
let repr: String?
let spoken: String?
}
struct Temperature: Codable {
let repr: String
}
struct Dewpoint: Codable {
let repr: String
}
struct Wxcodes: Codable {
let value: String
}
// WeatherModel
//
// WeatherModel.swift
// AvWx Pro
//
// Created by Grayson Bertaina on 9/22/20.
//
import Foundation
struct WeatherModel {
let reportingStation: String
let windGust: Int
let windSpeed: Int
let windDirection: String
let visibility: String
let flightRules: String
let time: String
let remarks: String
let temperature: String
let dewpoint: String
let rawMETAR: String
let lowestCloudsType: String
let lowestCloudsAlt: Int
let middleCloudsType: String
let middleCloudsAlt: Int
let highestCloudsType: String
let highestCloudsAlt: Int
let firstWxCode: String
var windGustString: String {
return String(format: "%u" + "kt", windGust)
}
var windSpeedString: String {
return String(format: "%u" + "kt", windSpeed)
}
var visUnits: String {
return visibility + " SM"
}
var degUnits: String {
return windDirection + "°"
}
var tempUnits: String {
return temperature + "°C"
}
var dewUnits: String {
return dewpoint + "°C"
}
var altToString1: String {
return String(format: "%u" + "00 ft", lowestCloudsAlt)
}
var altToString2: String {
return String(format: "%u" + "00 ft", middleCloudsAlt)
}
var altToString3: String {
return String(format: "%u" + "00 ft", highestCloudsAlt)
}
var flightConditions: String {
switch flightRules {
case "VFR":
return "green"
case "MVFR":
return "blue"
case "IFR":
return "red"
case "LIFR":
return "purple"
default:
return "gray"
}
}
}
private func parseJSON(_ weatherData: Data) -> WeatherModel? {
do {
let decoder = JSONDecoder()
let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
let clouds = decodedData.clouds
let wxcodes = decodedData.wx_codes
let reportingStationVar = decodedData.station ?? "N/A"
let windGustValue = decodedData.wind_gust?.value ?? 0
let windSpeedValue = decodedData.wind_speed?.value ?? 0
let windDirectionValue = decodedData.wind_direction?.repr ?? "N/A"
let visibilityValue = decodedData.visibility?.repr ?? "N/A"
let flightRulesValue = decodedData.flight_rules ?? "N/A"
let timeReportedMETAR = decodedData.time?.repr ?? "N/A"
let remarksReportedMETAR = decodedData.remarks ?? "N/A"
let tempMETAR = decodedData.temperature?.repr ?? "No Data"
let rawMETARData = decodedData.raw ?? "N/A"
let lowCloudsType = (clouds.count > 0 ? clouds[0]?.type : nil) ?? "N/A"
let midCloudsType = (clouds.count > 1 ? clouds[1]?.type : nil) ?? "N/A"
let highCloudsType = (clouds.count > 2 ? clouds[2]?.type : nil) ?? "N/A"
let lowCloudsAlt = (clouds.count > 0 ? clouds[0]?.altitude : nil) ?? 0
let midCloudsAlt = (clouds.count > 1 ? clouds[1]?.altitude : nil) ?? 0
let highCloudsAlt = (clouds.count > 2 ? clouds[2]?.altitude : nil) ?? 0
let firstWxCode1 = (wxcodes.count > 0 ? wxcodes[0]?.value : "N/A") ?? "N/A"
let weather = WeatherModel(reportingStation: reportingStationVar, windGust: windGustValue, windSpeed: windSpeedValue, windDirection: windDirectionValue, visibility: visibilityValue, flightRules: flightRulesValue, time: timeReportedMETAR, remarks: remarksReportedMETAR, temperature: tempMETAR, dewpoint: rawMETARData, rawMETAR: rawMETARData, lowestCloudsType: lowCloudsType, lowestCloudsAlt: lowCloudsAlt, middleCloudsType: midCloudsType, middleCloudsAlt: midCloudsAlt, highestCloudsType: highCloudsType, highestCloudsAlt: highCloudsAlt, firstWxCode: firstWxCode1)
return weather
} catch {
print(error)
return nil
}
}
You will notice that there are some useless functions, like trying fetchWeather at the top of my ContentView. I am trying various things to get data to be pulled but don't know where to put the required:
the fetchWeather function, and what to initialize
How to update the views and initialize the WeatherModel or WeatherManager to get that data.
I am new to Swift so I apologize if any part of my question is vague. I am definitely in a bit over my head! Thanks for the help and have a great day.
Add on:
//
// WeatherData.swift
// SwitUIAppExample
//
// Created by mac on 30/09/2020.
// Copyright © 2020 Kamran. All rights reserved.
//
//
// WeatherData.swift
// AvWx Pro
//
// Created by Grayson Bertaina on 9/21/20.
//
import Foundation
struct WeatherData: Codable {
let clouds: [Clouds?]
let flight_rules: String?
let remarks: String?
let wind_speed: WindSpeed?
let wind_gust: WindGust?
let wind_direction: WindDirection?
let visibility: Visibility?
let time: Time?
let station: String?
let altimeter: Altimeter?
let temperature: Temperature?
let dewpoint: Dewpoint?
let wx_codes: [Wxcodes?]
let raw: String?
}
struct Clouds: Codable {
let type: String
let altitude: Int
}
struct Time: Codable {
let repr: String
}
struct WindSpeed: Codable {
let value: Int
}
struct WindGust: Codable {
let value: Int
}
struct WindDirection: Codable {
let repr: String
}
struct Visibility: Codable {
let repr: String
}
struct Remarks: Codable {
let remarks: String
}
struct Altimeter: Codable {
let value: Double
}
struct Temperature: Codable {
let repr: String
}
struct Dewpoint: Codable {
let repr: String
}
struct Wxcodes: Codable {
let value: String
}
// WeatherModel
//
// WeatherModel.swift
// AvWx Pro
//
// Created by Grayson Bertaina on 9/22/20.
//
import Foundation
struct WeatherModel {
let lowestCloudsType: String
let lowestCloudsAlt: Int
let middleCloudsType: String
let middleCloudsAlt: Int
let highestCloudsType: String
let highestCloudsAlt: Int
let reportingStation: String
let windGust: Int
let windSpeed: Int
let windDirection: String
let visibility: String
let flightRules: String
let time: String
let remarks: String
let altimeter: Double
let temperature: String
let dewpoint: String
let firstWxCode: String
let rawMETAR: String
var altToString1: String {
return String(format: "%u" + "00 ft", lowestCloudsAlt)
}
var altToString2: String {
return String(format: "%u" + "00 ft", middleCloudsAlt)
}
var altToString3: String {
return String(format: "%u" + "00 ft", highestCloudsAlt)
}
var windGustString: String {
return String(format: "%u" + "kt", windGust)
}
var windSpeedString: String {
return String(format: "%u" + "kt", windSpeed)
}
var altimeterString: String {
return String(format: "%.2f" + " inHg", altimeter as CVarArg)
}
var visUnits: String {
return visibility + " SM"
}
var degUnits: String {
return windDirection + "°"
}
var tempUnits: String {
return temperature + "°C"
}
var dewUnits: String {
return dewpoint + "°C"
}
var flightConditions: String {
switch flightRules {
case "VFR":
return "green"
case "MVFR":
return "blue"
case "IFR":
return "red"
case "LIFR":
return "purple"
default:
return "gray"
}
}
}
// WeatherManager
//
// WeatherManager.swift
// AvWx Pro
//
// Created by Grayson Bertaina on 9/21/20.
//
import Foundation
class WeatherManager: ObservableObject {
#Published var weater: WeatherModel?
let weatherURL = "https://avwx.rest/api/metar/"
func fetchWeather (stationICAO: String) {
let urlString = "\(weatherURL)\(stationICAO)?token=OVi45FiTDo1LmyodShfOfoizNe5m9wyuO6Mkc95AN-c"
performRequest(with: urlString)
}
func performRequest (with urlString: String) {
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error)
return
}
if let safeData = data {
self.weater = self.parseJSON(safeData)
}
}
task.resume()
print(urlString)
}
}
private func parseJSON(_ weatherData: Data) -> WeatherModel? {
do {
let decoder = JSONDecoder()
let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
let clouds = decodedData.clouds
let wxcodes = decodedData.wx_codes
let lowCloudsType = (clouds.count > 0 ? clouds[0]?.type : nil) ?? "N/A"
let midCloudsType = (clouds.count > 1 ? clouds[1]?.type : nil) ?? "N/A"
let highCloudsType = (clouds.count > 2 ? clouds[2]?.type : nil) ?? "N/A"
let lowCloudsAlt = (clouds.count > 0 ? clouds[0]?.altitude : nil) ?? 0
let midCloudsAlt = (clouds.count > 1 ? clouds[1]?.altitude : nil) ?? 0
let highCloudsAlt = (clouds.count > 2 ? clouds[2]?.altitude : nil) ?? 0
let reportingStationVar = decodedData.station ?? "N/A"
let windGustValue = decodedData.wind_gust?.value ?? 0
let windSpeedValue = decodedData.wind_speed?.value ?? 0
let windDirectionValue = decodedData.wind_direction?.repr ?? "N/A"
let visibilityValue = decodedData.visibility?.repr ?? "N/A"
let flightRulesValue = decodedData.flight_rules ?? "N/A"
let timeReportedMETAR = decodedData.time?.repr ?? "N/A"
let remarksReportedMETAR = decodedData.remarks ?? "N/A"
let altimeterMETAR = decodedData.altimeter?.value ?? 0
let tempMETAR = decodedData.temperature?.repr ?? "No Data"
let dewMETAR = decodedData.dewpoint?.repr ?? "No Data"
let firstWxCode1 = (wxcodes.count > 0 ? wxcodes[0]?.value : "N/A") ?? "N/A"
let rawMETARData = decodedData.raw ?? "N/A"
let weather = WeatherModel(lowestCloudsType: lowCloudsType , lowestCloudsAlt: lowCloudsAlt, middleCloudsType: midCloudsType , middleCloudsAlt: midCloudsAlt, highestCloudsType: highCloudsType , highestCloudsAlt: highCloudsAlt, reportingStation: reportingStationVar, windGust: windGustValue, windSpeed: windSpeedValue, windDirection: windDirectionValue, visibility: visibilityValue, flightRules: flightRulesValue, time: timeReportedMETAR, remarks: remarksReportedMETAR, altimeter: altimeterMETAR, temperature: tempMETAR, dewpoint: dewMETAR, firstWxCode: firstWxCode1, rawMETAR: rawMETARData)
return weather
} catch {
print(error)
return nil
}
}
}
You need to introduce few changes as below,
Change WeatherManager to class, conform to ObservableObject and introduce weather model as below,
class WeatherManager: ObservableObject {
#Published var weather: WeatherModel?
}
Rest of the WeatherManager will be same except that you need to set the above property after parsing inside performRequest method as below,
if let safeData = data {
self.weather = self.parseJSON(safeData)
}
Now change METARReport by introducing var model: WeatherModel? and set all the labels same as time shown below,
struct METARReport: View {
var model: WeatherModel?
var body: some View {
ScrollView {
VStack{
HStack{
Text("METAR Report")
.font(.title)
Spacer()
Text("Time: " + (self.model?.time ?? ""))
.font(.headline)
}
.....
}
Finally just update METARView as below,
struct METARView: View {
#State var weatherManager = WeatherManager()
init(search: String) {
self.weatherManager.fetchWeather(stationICAO: search)
}
var body: some View {
ScrollView{
// ...
METARReport(model: weatherManager.weather)
Spacer()
}
.edgesIgnoringSafeArea(/*#START_MENU_TOKEN#*/.all/*#END_MENU_TOKEN#*/)
}
}
Note: There were some parsing issues as well in your code so i used the below models to make it work.
struct WeatherData: Codable {
let flight_rules: String?
let remarks: String?
let wind_speed: WindSpeed?
let wind_gust: WindGust?
let wind_direction: WindDirection?
let visibility: Visibility?
let time: Time?
let station: String?
let temperature: Temperature?
let raw: String?
}
struct Clouds: Codable {
let type: String
let altitude: Int
}
struct Time: Codable {
let repr: String
}
struct WindSpeed: Codable {
let value: Int
}
struct WindGust: Codable {
let value: Int
}
struct WindDirection: Codable {
let repr: String
}
struct Visibility: Codable {
let repr: String
}
struct Remarks: Codable {
let remarks: String
}
struct Altimeter: Codable {
let repr: String?
let spoken: String?
}
struct Temperature: Codable {
let repr: String
}
struct Dewpoint: Codable {
let repr: String
}
struct Wxcodes: Codable {
let value: String
}
// WeatherModel
//
// WeatherModel.swift
// AvWx Pro
//
// Created by Grayson Bertaina on 9/22/20.
//
import Foundation
struct WeatherModel {
let reportingStation: String
let windGust: Int
let windSpeed: Int
let windDirection: String
let visibility: String
let flightRules: String
let time: String
let remarks: String
let temperature: String
let dewpoint: String
let rawMETAR: String
var windGustString: String {
return String(format: "%u" + "kt", windGust)
}
var windSpeedString: String {
return String(format: "%u" + "kt", windSpeed)
}
var visUnits: String {
return visibility + " SM"
}
var degUnits: String {
return windDirection + "°"
}
var tempUnits: String {
return temperature + "°C"
}
var dewUnits: String {
return dewpoint + "°C"
}
var flightConditions: String {
switch flightRules {
case "VFR":
return "green"
case "MVFR":
return "blue"
case "IFR":
return "red"
case "LIFR":
return "purple"
default:
return "gray"
}
}
}
private func parseJSON(_ weatherData: Data) -> WeatherModel? {
do {
let decoder = JSONDecoder()
let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
let reportingStationVar = decodedData.station ?? "N/A"
let windGustValue = decodedData.wind_gust?.value ?? 0
let windSpeedValue = decodedData.wind_speed?.value ?? 0
let windDirectionValue = decodedData.wind_direction?.repr ?? "N/A"
let visibilityValue = decodedData.visibility?.repr ?? "N/A"
let flightRulesValue = decodedData.flight_rules ?? "N/A"
let timeReportedMETAR = decodedData.time?.repr ?? "N/A"
let remarksReportedMETAR = decodedData.remarks ?? "N/A"
let tempMETAR = decodedData.temperature?.repr ?? "No Data"
let rawMETARData = decodedData.raw ?? "N/A"
let weather = WeatherModel(reportingStation: reportingStationVar, windGust: windGustValue, windSpeed: windSpeedValue, windDirection: windDirectionValue, visibility: visibilityValue, flightRules: flightRulesValue, time: timeReportedMETAR, remarks: remarksReportedMETAR, temperature: tempMETAR, dewpoint: rawMETARData, rawMETAR: rawMETARData)
return weather
} catch {
print(error)
return nil
}
}

Resources