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))
}
}
Related
I have an array of reminders(reminder model) in a view model and want to be able to edit existing reminders specifically through and edit swipe action and then through the reminders detail screen. I tried adding a button with a sheet to my Homeview in a list and then tried updating the edited reminder in the reminders array to a property in my view model called existingRemindData by using an update function in the reminder model. this should work but the remind var created by the foreach loop in the home view doesn't keep its value when it is called in the sheet. In the home view under the edit swipe action when I assign homevm.existingRemindData = remind.data it is equal to whatever reminder I swipe on because I did a print statement to confirm but as soon as I try to use the remind var inside of the sheet for the edit action the remind var defaults to the first item in the reminder array in the view model which is obviously not right. how would I make it so it uses the correct reminder index value when trying to update the reminder or is there another way which I could implement this functionality. any help would be great and look in the code for clarification on what I talk about.
HomeView
'''
import SwiftUI
struct HomeView: View {
#StateObject private var homeVM = HomeViewModel()
#State var percent: Int = 1
#State var showDetailEditView = false
#State var showAddView = false
#State var dropDown = false
//#State var filter = false
var body: some View {
ZStack {
VStack {
List {
ForEach($homeVM.reminds) { $remind in
ReminderView(remind: $remind)
//.background(remind.theme.mainColor)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.swipeActions(edge: .leading) {
Button(action: {
self.showDetailEditView.toggle()
homeVM.existingRemindData = remind.data
print(homeVM.existingRemindData.title)
}) {
Label("Edit", systemImage: "pencil")
}
}
.sheet(isPresented: $showDetailEditView) {
NavigationView {
ReminderEditView(data: $homeVM.existingRemindData)
.navigationTitle(homeVM.existingRemindData.title)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
self.showDetailEditView.toggle()
homeVM.existingRemindData = Reminder.Data()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
self.showDetailEditView.toggle()
print("\(remind.id) \(remind.title)")
print("\(homeVM.existingRemindData.id) \(homeVM.existingRemindData.title)")
remind.update(from: homeVM.existingRemindData)
homeVM.newRemindData = Reminder.Data()
}
}
}
.background(LinearGradient(gradient: Gradient(colors: [
Color(UIColor(red: 0.376, green: 0.627, blue: 0.420, alpha: 1)),
Color(UIColor(red: 0.722, green: 0.808, blue: 0.725, alpha: 1))
]), startPoint: .topLeading, endPoint: .bottomTrailing))
}
}
.swipeActions(allowsFullSwipe: true) {
Button (role: .destructive, action: {
homeVM.deleteReminder(remind: remind)
}) {
Label("Delete", systemImage: "trash.fill")
}
}
}
}
.onAppear(
perform: {
UITableView.appearance().backgroundColor = .clear
UITableViewCell.appearance().backgroundColor = .clear
})
'''
Reminder edit view
'''
import SwiftUI
extension Binding {
static func ??(lhs: Binding<Optional<Value>>, rhs: Value) -> Binding<Value> {
return Binding(get: {lhs.wrappedValue ?? rhs}, set: {lhs.wrappedValue = $0})
}
}
struct ReminderEditView: View {
#ObservedObject var editVM: EditViewModel
init(data: Binding<Reminder.Data>) {
editVM = EditViewModel(data: data)
}
var body: some View {
Form {
Section {
TextField("Title", text: $editVM.data.title)
TextField("Notes", text: $editVM.data.notes ?? "")
.frame(height: 100, alignment: .top)
}
Section {
Toggle(isOn: $editVM.data.hasDueDate, label: {
if editVM.data.hasDueDate {
VStack(alignment: .leading) {
Text("Date")
Text(editVM.data.hasDueDate ? editVM.data.formatDate(date: editVM.data.date!) : "\(editVM.data.formatDate(date: Date.now))")
.font(.caption)
.foregroundColor(.red)
}
} else {
Text("Date")
}
})
if editVM.data.hasDueDate {
DatePicker("Date", selection: $editVM.data.dueDate, in: Date()..., displayedComponents: .date)
.datePickerStyle(.graphical)
}
'''
Reminder model
'''
extension Reminder {
struct Data: Identifiable {
var title: String = ""
var notes: String?
var date: Date?
var time: Date?
var theme: Theme = .poppy
var iscomplete: Bool = false
var priority: RemindPriority = .None
let id: UUID = UUID()
var dueDate: Date {
get {
return date ?? Date()
}
set {
date = newValue
}
}
var dueTime: Date {
get {
return time ?? Date()
}
set {
time = newValue
}
}
func formatDate(date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .full
formatter.timeStyle = .none
return formatter.string(from: date)
}
func formatTime(time: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
return formatter.string(from: time)
}
var hasDueDate: Bool {
get {
date != nil
}
set {
if newValue == true {
date = Date()
}
else {
date = nil
hasDueTime = false
}
}
}
var hasDueTime: Bool {
get {
time != nil
}
set {
if newValue == true {
time = Date()
hasDueDate = true
}
else {
time = nil
}
}
}
}
var data: Data {
Data(title: title, notes: notes, date: date, time: time, theme: theme, iscomplete: iscomplete, priority: priority)
}
mutating func update(from data: Data) {
title = data.title
notes = data.notes
date = data.date
time = data.time
theme = data.theme
iscomplete = data.iscomplete
priority = data.priority
}
init(data: Data) {
title = data.title
notes = data.notes
date = data.date
time = data.time
theme = data.theme
iscomplete = data.iscomplete
priority = data.priority
id = data.id
}
}
'''
HomeViewModel(View model talked about)
'''
import Foundation
import SwiftUI
import Combine
class HomeViewModel: ObservableObject {
#Published var reminds: [Reminder] = Reminder.sampleReminders
#Published var newRemindData = Reminder.Data()
#Published var existingRemindData = Reminder.Data()
#Published var selectedRemind = Reminder(data: Reminder.Data())
#Published var compReminds: [Reminder] = []
private var cancellables = Set<AnyCancellable>()
/*init(reminds: [Reminder]) {
self.reminds = reminds
}*/
func newReminder() {
let newRemind = Reminder(data: newRemindData)
reminds.append(newRemind)
newRemindData = Reminder.Data()
}
func deleteReminder(remind: Reminder) {
Just(remind)
.delay(for: .seconds(0.25), scheduler: RunLoop.main)
.sink {remind in
if remind.iscomplete {
self.removeRemind(remind: remind)
}
if !remind.iscomplete {
self.removeRemind(remind: remind)
}
self.reminds.removeAll { $0.id == remind.id }
}
.store(in: &cancellables)
}
func appendRemind(complete: Reminder) {
compReminds.append(complete)
}
func removeRemind(remind: Reminder) {
compReminds.removeAll() { $0.id == remind.id }
}
func remindIndex() -> Int {
return reminds.firstIndex(where: {
$0.id == existingRemindData.id
}) ?? 1
}
We don't use view model objects in SwiftUI. Change EditViewModel class to be an EditConfig struct, declare it as #State var config: EditConfig? use it as the item in sheet(item:onDismiss:content:) instead of the bool version.
Also, your date formatting is not SwiftUI compatible, you won't benefit from the labels being updated automatically when the user changes their region settings. To fix that remove the formatDate code and instead supply the formatter to Text or simply use .date. If using a formatter object make sure you aren't initing a new one every time, e.g. store one inside an #State struct or a static var. in SwiftUI we must not init objects in a View's init and body, only value types.
I have a widget with a SwiftUI Text timer date. It countdowns as expected. However, when the device is locked, the timer is frozen.
To reproduce it, add the SwiftUI Text(_:style:) view to a widget and place the widget on the "Today View". The countdown should work as expected. However, lock the phone then view the Today View in a locked state. The timer is frozen.
Below is the full working sample code:
import WidgetKit
import SwiftUI
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date())
}
func getSnapshot(in context: Context, completion: #escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date())
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 hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
}
struct TestWidget123EntryView : View {
var entry: Provider.Entry
var body: some View {
Text(entry.date, style: .timer)
.multilineTextAlignment(.center)
.padding()
}
}
#main
struct TestWidget123: Widget {
let kind: String = "TestWidget123"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
TestWidget123EntryView(entry: entry)
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}
struct TestWidget123_Previews: PreviewProvider {
static var previews: some View {
TestWidget123EntryView(entry: SimpleEntry(date: Date()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
I don't have any data protected data and I tried using a timer but didn't work at all. How can I allow the Text timer to countdown in a locked state?
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.
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))
}
}
I am creating a list that loads data when the user reaches the bottom of the list. I can crash the app when I load more elements and long-press an element within the list. The view is wrapped in a NavigationView and a NavigationLink. When the app crashes, you get EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0) with the thread 1 specialized saying "RandomAccessCollection<>.index(_:offsetBy:))". Looking into the EXC_BAD_INSTRUCTION I thought it could be force unwrapping, but I don't see anywhere in the code that could cause this issue.
The issue only occurs on an iPad and happens randomly. With WWDC being yesterday, I thought this would have been fixed, so we downloaded the beta for Xcode 12, and this error still occurs.
Here is the full code:
import UIKit
import SwiftUI
import Combine
struct ContentView: View {
var body: some View {
RepositoriesListContainer(viewModel: RepositoriesViewModel())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
enum GithubAPI {
static let pageSize = 10
static func searchRepos(query: String, page: Int) -> AnyPublisher<[Repository], Error> {
let url = URL(string: "https://api.github.com/search/repositories?q=\(query)&sort=stars&per_page=\(Self.pageSize)&page=\(page)")!
return URLSession.shared
.dataTaskPublisher(for: url) // 1.
.tryMap { try JSONDecoder().decode(GithubSearchResult<Repository>.self, from: $0.data).items } // 2.
.receive(on: DispatchQueue.main) // 3.
.eraseToAnyPublisher()
}
}
struct GithubSearchResult<T: Codable>: Codable {
let items: [T]
}
struct Repository: Codable, Identifiable, Equatable {
let id: Int
let name: String
let description: String?
let stargazers_count: Int
}
class RepositoriesViewModel: ObservableObject {
#Published private(set) var state = State()
private var subscriptions = Set<AnyCancellable>()
// 2.
func fetchNextPageIfPossible() {
guard state.canLoadNextPage else { return }
GithubAPI.searchRepos(query: "swift", page: state.page)
.sink(receiveCompletion: onReceive,
receiveValue: onReceive)
.store(in: &subscriptions)
}
private func onReceive(_ completion: Subscribers.Completion<Error>) {
switch completion {
case .finished:
break
case .failure:
state.canLoadNextPage = false
}
}
private func onReceive(_ batch: [Repository]) {
state.repos += batch
state.page += 1
state.canLoadNextPage = batch.count == GithubAPI.pageSize
}
// 3.
struct State {
var repos: [Repository] = []
var page: Int = 1
var canLoadNextPage = true
}
}
struct RepositoriesListContainer: View {
#ObservedObject var viewModel: RepositoriesViewModel
var body: some View {
RepositoriesList(
repos: viewModel.state.repos,
isLoading: viewModel.state.canLoadNextPage,
onScrolledAtBottom: viewModel.fetchNextPageIfPossible
)
.onAppear(perform: viewModel.fetchNextPageIfPossible)
}
}
struct RepositoriesList: View {
// 1.
let repos: [Repository]
let isLoading: Bool
let onScrolledAtBottom: () -> Void // 2.
var body: some View {
NavigationView {
List {
reposList
if isLoading {
loadingIndicator
}
}
}
// .OnlyStackNavigationView()
}
private var reposList: some View {
ForEach(repos) { repo in
// 1.
RepositoryRow(repo: repo).onAppear {
// 2.
if self.repos.last == repo {
self.onScrolledAtBottom()
}
}
.onTapGesture {
print("TAP")
}
.onLongPressGesture {
print("LONG PRESS")
}
}
}
private var loadingIndicator: some View {
Spinner(style: .medium)
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
}
}
struct RepositoryRow: View {
let repo: Repository
var body: some View {
NavigationLink(destination: LandmarkDetail()){VStack {
Text(repo.name).font(.title)
Text("⭐️ \(repo.stargazers_count)")
repo.description.map(Text.init)?.font(.body)
}}
}
}
struct Spinner: UIViewRepresentable {
let style: UIActivityIndicatorView.Style
func makeUIView(context: Context) -> UIActivityIndicatorView {
let spinner = UIActivityIndicatorView(style: style)
spinner.hidesWhenStopped = true
spinner.startAnimating()
return spinner
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {}
}
struct LandmarkDetail: View {
var body: some View {
VStack {
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack(alignment: .top) {
Text("Joshua Tree National Park")
.font(.subheadline)
Spacer()
Text("California")
.font(.subheadline)
}
}
.padding()
Spacer()
}
}
}