Implementation StoreKit 2 Swiftui - swift

Hi in this view I have to load the purchases present on storekit, on the try await self.puchase(products) line I have the following error: alue of type 'SettingsForm' has no member 'purchase' how do I fix it and load the products on startup of the view?
Settings code:
import SwiftUI
import Foundation
import Backend
import StoreKit
#available(iOS 15.0, *)
#available(iOS 15.0, *)
struct SettingsForm : View {
#State var selectedRegion: Int = 0
#State var alwaysOriginalTitle: Bool = false
#State
private var products: [Product] = []
#Environment(\.presentationMode) var presentationMode
let productIds = ["premium"]
private func loadProducts() async throws {
self.products = try await Product.products(for: productIds)
print(self.products)
}
var countries: [String] {
get {
var countries: [String] = []
for code in NSLocale.isoCountryCodes {
let id = NSLocale.localeIdentifier(fromComponents: [NSLocale.Key.countryCode.rawValue: code])
let name = NSLocale(localeIdentifier: "en_US").displayName(forKey: NSLocale.Key.identifier, value: id)!
countries.append(name)
}
return countries
}
}
func debugInfoView(title: String, info: String) -> some View {
HStack {
Text(title)
Spacer()
Text(info).font(.body).foregroundColor(.secondary)
}
}
var body: some View {
NavigationView {
Form {
Section(header: Text("Abbonamenti"),
footer: Text(""),
content: {
ForEach(self.products) { (products) in
Button {
Task {
do {
try await self.purchase(products)
} catch {
print(error)
}
}
} label: {
Text("\(products.displayPrice) - \(products.displayName)")
}
}
})
Section(header: Text("Region preferences"),
footer: Text("Region is used to display a more accurate movies list"),
content: {
Toggle(isOn: $alwaysOriginalTitle) {
Text("Always show original title")
}
Picker(selection: $selectedRegion,
label: Text("Region"),
content: {
ForEach(0 ..< self.countries.count) {
Text(self.countries[$0]).tag($0)
}
})
})
Section(header: Text("App data"), footer: Text("None of those action are working yet ;)"), content: {
Text("Export my data")
Text("Backup to iCloud")
Text("Restore from iCloud")
Text("Reset application data").foregroundColor(.red)
})
Section(header: Text("Debug info")) {
debugInfoView(title: "Movies in state",
info: "\(store.state.moviesState.movies.count)")
debugInfoView(title: "Archived state size",
info: "\(store.state.sizeOfArchivedState())")
}
}
.onAppear{
if let index = NSLocale.isoCountryCodes.firstIndex(of: AppUserDefaults.region) {
self.selectedRegion = index
}
self.alwaysOriginalTitle = AppUserDefaults.alwaysOriginalTitle
}
.navigationBarItems(
leading: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Cancel").foregroundColor(.red)
}),
trailing: Button(action: {
AppUserDefaults.region = NSLocale.isoCountryCodes[self.selectedRegion]
AppUserDefaults.alwaysOriginalTitle = self.alwaysOriginalTitle
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text("Save")
}))
.navigationBarTitle(Text("Settings"))
}
}
}
#if DEBUG
struct SettingsForm_Previews : PreviewProvider {
#available(iOS 14.0, *)
static var previews: some View {
if #available(iOS 15.0, *) {
SettingsForm()
} else {
// Fallback on earlier versions
}
}
}
#endif

In StoreKit2, to purchase a Product, use…
let purchaseResult = try await product.purchase()

Related

Binding’s inside NavigationSplitView detail (TextField, TextEditor)

I'm using a two-column NavigationSplitView. Trying to figure out how to update the data model via .onSubmit modifier and use a TextField view without Binding.constant.
Within the detail section, I have TextField and TextEditor.
How to avoid Binding.contant()? I mean, I need mutation.
This is a correct way to update value property in Model?
I need a single selection in List.
Here's my sample code (70 line’s):
struct Model: Identifiable, Hashable {
var id = UUID()
var title: String = "Brand new"
var value: String = ""
func updateValue() async -> Model {
return Model(id: id, title: title, value: "The boar 🐗 is running through the field happily")
}
}
final class DataModel: ObservableObject {
#Published
var models: [Model] = [
.init(title: "First", value: "fur"),
.init(title: "Second", value: "meow"),
.init(title: "Another", value: "Make SwiftUI, not war")
]
#MainActor
func updateModel(for model: Model.ID) async -> Void {
var findModel = models.first { $0.id == model }
findModel = await findModel?.updateValue()
}
}
struct ModelView: View {
#StateObject
private var dataModel = DataModel()
#State
private var listSelection: Model.ID?
private var selectedModel: Model? {
guard let selection = listSelection else { return nil }
return dataModel.models.first { $0.id == selection }
}
var body: some View {
NavigationSplitView {
List(dataModel.models, selection: $listSelection) { model in
NavigationLink(model.title, value: model.id)
}
} detail: {
if let selectedModel {
VStack {
TextField("Title", text: .constant(selectedModel.title))
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20))
.submitLabel(.go)
.onSubmit {
Task {
// Update Model.value by hit `Go`
await dataModel.updateModel(for: selectedModel.id)
}
}
TextEditor(text: .constant(selectedModel.value))
}
.padding()
.navigationTitle(selectedModel.title)
}
}
}
}
struct ModelView_Previews: PreviewProvider {
static var previews: some View {
ModelView()
.colorScheme(.light)
}
}
After a couple of days, I realized what I could do.
No one answered the question, so I solved the problem this way.
The final solution is below:
struct Model: Identifiable, Hashable {
var id = UUID()
var title: String = "Brand new"
var value: String = ""
func updateValue() async -> Model {
return Model(id: id, title: title, value: "The boar 🐗 is running through the field happily")
}
}
final class DataModel: ObservableObject {
#Published
var models: [Model] = [
.init(title: "First", value: "fur"),
.init(title: "Second", value: "meow"),
.init(title: "Another", value: "Make SwiftUI, not war")
]
#MainActor
func updateModel(for model: Binding<Model>) async -> Void {
model.wrappedValue = await model.wrappedValue.updateValue()
}
func bindingToModel(_ model: Model.ID) -> Binding<Model> {
Binding<Model> {
guard let index = self.models.firstIndex(where: { $0.id == model }) else {
return Model()
}
return self.models[index]
} set: { newModel in
guard let index = self.models.firstIndex(where: { $0.id == model }) else { return }
self.models[index] = newModel
}
}
}
struct ModelView: View {
#StateObject
private var dataModel = DataModel()
#State
private var listSelection: Model.ID?
var body: some View {
NavigationSplitView {
List(dataModel.models, selection: $listSelection) { model in
NavigationLink(model.title, value: model.id)
}
} detail: {
if let listSelection, let bindModel = dataModel.bindingToModel(listSelection) {
VStack {
TextField("Title", text: bindModel.title)
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20))
.submitLabel(.go)
.onSubmit {
Task {
// Update Model.value by hit `Go`
await dataModel.updateModel(for: bindModel)
}
}
TextEditor(text: bindModel.value)
}
.padding()
.navigationTitle(bindModel.title)
}
}
}
}

How to keep the selected item on DetailView when picker selection changed

I have a NavigationSplitView with a Picker that shows 2 list on a MainView. When you select an item, the DetailView is update. The problem is that when you change the picker selection the DetailView not always show a value.
I want to keep the DetailView until the user select other value.
This a little example code that shows the problem.
import SwiftUI
struct Model1: Identifiable, Hashable {
let id = UUID()
let title: String
let description: String
}
struct Model2: Identifiable, Hashable {
let id = UUID()
let name: String
let address: String
}
struct CustomData: Identifiable {
var id = UUID()
var docs: [Model1]
var lites: [Model2]
init(docs: [Model1], lites: [Model2]) {
self.docs = docs
self.lites = lites
}
}
#MainActor
final class TestViewModel: ObservableObject {
#Published var data: CustomData?
func loadData() {
//FetchData
let docs = [Model1(title: "title1", description: "desc1"), Model1(title: "title2", description: "desc2"), Model1(title: "title3", description: "desc3")]
let lites = [Model2(name: "value1", address: "desc1"), Model2(name: "value2", address: "desc2"), Model2(name: "value3", address: "desc3")]
self.data = CustomData(docs: docs, lites: lites)
}
func lites() -> [Model2] {
return data?.lites ?? []
}
func docs() -> [Model1] {
return data?.docs ?? []
}
}
struct TestView2: View {
#State private var subpage = 0
#ObservedObject var viewModel = TestViewModel()
var body: some View {
NavigationSplitView {
VStack {
Picker("Select option", selection: $subpage) {
Text("Lites").tag(0)
Text("Docs").tag(1)
}
.pickerStyle(.segmented)
//
VStack() {
switch(subpage){
case 0: //List of Lites
List(viewModel.lites()) { lite in
NavigationLink(value: lite) {
HStack (alignment: .top) {
Text(lite.name)
.padding(.top, 10)
.padding(.leading, 5)
.font(.system(.headline))
}
}
}
.navigationDestination(for: Model2.self) { lite in
DetailView2(path: lite.name)
}
case 1:
List(viewModel.docs()) { doc in
NavigationLink(value: doc) {
HStack (alignment: .top) {
Text(doc.title)
.padding(.top, 10)
.padding(.leading, 5)
.font(.system(.subheadline))
}
}
}
.navigationDestination(for: Model1.self) { doc in
DetailView2(path: doc.title)
}
default:
Text("No docs")
}
}
.accentColor(AppColors.LightBlue)
.padding(.top, 10)
.scrollContentBackground(.hidden)
.background(AppColors.LightGray)
}
}
detail: {
DetailView2(path: "Example Text")
}
.onAppear(){
viewModel.loadData()
}
}
}
struct DetailView2: View {
var path: String
var body: some View {
VStack(spacing: .infinity) {
Text(path)
}
.navigationBarHidden(true)
}
}
struct TestView2_Previews: PreviewProvider {
static var previews: some View {
TestView2()
.previewInterfaceOrientation(.landscapeRight)
.previewDevice("iPad (9th generation)")
}
}
You need to add a destination navigationDestination to your VStack like this
import SwiftUI
struct Model1: Identifiable, Hashable {
let id = UUID()
let title: String
let description: String
}
struct Model2: Identifiable, Hashable {
let id = UUID()
let name: String
let address: String
}
struct CustomData: Identifiable {
var id = UUID()
var docs: [Model1]
var lites: [Model2]
init(docs: [Model1], lites: [Model2]) {
self.docs = docs
self.lites = lites
}
}
#MainActor
final class TestViewModel: ObservableObject {
#Published var data: CustomData?
func loadData() {
//FetchData
let docs = [Model1(title: "title1", description: "desc1"), Model1(title: "title2", description: "desc2"), Model1(title: "title3", description: "desc3")]
let lites = [Model2(name: "value1", address: "desc1"), Model2(name: "value2", address: "desc2"), Model2(name: "value3", address: "desc3")]
self.data = CustomData(docs: docs, lites: lites)
}
func lites() -> [Model2] {
return data?.lites ?? []
}
func docs() -> [Model1] {
return data?.docs ?? []
}
}
#available(iOS 16.0, *)
struct TestView2: View {
#State private var subpage = 0
#ObservedObject var viewModel = TestViewModel()
var body: some View {
NavigationSplitView {
VStack {
Picker("Select option", selection: $subpage) {
Text("Lites").tag(0)
Text("Docs").tag(1)
}
.pickerStyle(.segmented)
//
VStack() {
switch(subpage){
case 0: //List of Lites
List(viewModel.lites()) { lite in
NavigationLink(value: lite) {
HStack (alignment: .top) {
Text(lite.name)
.padding(.top, 10)
.padding(.leading, 5)
.font(.system(.headline))
}
}
}
case 1:
List(viewModel.docs()) { doc in
NavigationLink(value: doc) {
HStack (alignment: .top) {
Text(doc.title)
.padding(.top, 10)
.padding(.leading, 5)
.font(.system(.subheadline))
}
}
}
default:
Text("No docs")
}
}
.accentColor(.blue)
.padding(.top, 10)
.scrollContentBackground(.hidden)
.background(.gray)
.navigationDestination(for: Model1.self) { doc in
DetailView2(path: doc.title)
}
.navigationDestination(for: Model2.self) { lite in
DetailView2(path: lite.name)
}
}
}
detail: {
DetailView2(path: "Example Text")
}
.onAppear(){
viewModel.loadData()
}
}
}
struct DetailView2: View {
var path: String
var body: some View {
VStack(spacing: .infinity) {
Text(path)
}
.navigationBarHidden(true)
}
}
#available(iOS 16.0, *)
struct TestView2_Previews: PreviewProvider {
static var previews: some View {
TestView2()
.previewInterfaceOrientation(.landscapeRight)
.previewDevice("iPad (9th generation)")
}
}

SwiftUI Modifying state during view update, this will cause undefined behavior - Proper use explanation

Im learning swift and this error/warning is driving me crazy because I cant see what call Im making that causing it... The Xcode warning only shows up in my #main struct
Modifying state during view update, this will cause undefined behavior.
I thought it might be in the ListView, but I realized the warning only shows after the "Submit Post" button is it.
Im looking for a fix, but more importantly and explanation as to why this is happening and the proper usage moving forward.
import SwiftUI
import Firebase
#main
struct SocialcademyApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
PostsList()
}
}
}
struct PostsList: View {
#StateObject var viewModel = PostsViewModel()
#State private var searchText = ""
#State private var showNewPostForm = false
var body: some View {
NavigationView {
List(viewModel.posts) { post in
if searchText.isEmpty || post.contains(searchText) {
PostRow(post: post)
}
}
.searchable(text: $searchText)
.navigationTitle("Posts")
.toolbar {
Button {
showNewPostForm = true
} label: {
Label("New Post", systemImage: "square.and.pencil")
}
}
.sheet(isPresented: $showNewPostForm) {
NewPostView(creationAction: viewModel.makeCreationAction())
}
}
}
}
struct NewPostView: View {
typealias CreationAction = (Post) async throws -> Void
let creationAction: CreationAction
#State private var post = Post(title: "", content: "", authorName: "")
#State private var state = FormState.idle
#Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
Form {
Section {
TextField("Title", text: $post.title)
TextField("Author Name", text: $post.authorName)
}
Section {
TextField("Content", text: $post.content)
.multilineTextAlignment(.leading)
}
Button(action: createPost, label: {
if state == .working {
ProgressView() } else {
Text("Submit Post")
}
})
.font(.headline)
.frame(maxWidth: .infinity)
.foregroundColor(.white)
.padding()
.listRowBackground(Color.accentColor)
}
}
.navigationTitle("New Post")
.disabled(state == .working)
.alert("Cannot Create Post", isPresented: $state.isError, actions: {}) {
Text("Sorry, something went wrong")
}
.onSubmit {
createPost()
}
}
private func createPost() {
print("[NewPostForm] creating a new post")
Task {
state = .working
do {
try await creationAction(post)
dismiss()
} catch {
state = .error
print("[NewPostForm] Cannot create post: \(error)")
}
}
}
}
private extension NewPostView {
enum FormState {
case idle, working, error
var isError: Bool {
get {
self == .error
}
set {
guard !newValue else { return }
self = .idle
}
}
}
}
#MainActor
class PostsViewModel: ObservableObject {
#Published var posts = [Post.testPost]
func makeCreationAction() -> NewPostView.CreationAction {
return { [weak self] post in
try await PostsRepository.create(post)
self?.posts.insert(post, at: 0)
}
}
}

SwiftUI - How to toogle all the booleans in a CoreData

I have a database with several objects with booleans as attribute. I'm looking for a function to invert all boolean objects when I press a button. I tried this function but several errors are displayed like (Value of type 'Bool' has no member 'indices') :
struct ViewList: View {
#Environment(\.managedObjectContext) var context
#State var newName: String = ""
#FetchRequest(
entity: Product.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Product.name, ascending: true)]
) var list: FetchedResults<Product>
var body: some View {
VStack {
HStack {
TextField("I insert the name of the product", text: $newName)
Button(action: { self.add()
self.newName = ""
})
{ Image(systemName: "plus") }
}
List {
ForEach(list, id: \.self) {
product in ViewItem(product: product)
}
}
}
}
public func add() {
let newProduct = Product(context: context)
newProduct.name = newName
do {
try context.save()
} catch {
print(error)
}
}
}
struct ViewItem: View {
#State var product: Product
#State var refresh: Bool = false
var body: some View {
NavigationLink(destination: ViewDetail(product: product, refresh: $refresh)) {
HStack(alignment: .top) {
Button( action: {
self.clean()
self.product.isSelected.toggle()
}) {
if self.product.isSelected == true {
Image(systemName: "checkmark")
} else {
Image(systemName: "checkmark").colorInvert()
}
}
VStack() {
Text(product.name)
if product.password != "" {
Text("Password : " + product.password)
}
Text(String(refresh)).hidden()
}
}
}
.onAppear {
self.refresh = false
}
}
}
I've been thinking about it, but I don't know how to go about it...
func clean() {
for( index ) in self.product.isSelected.indices {
self.product[index]isSelected = false
}
}
You need to create a query to flip the state of the isSelected flag. This logic is best kept out of the view system so you can use it anywhere.
You create a SelectionHandler
import CoreData
class SelectionHandler {
func clearSelection(in context: NSManagedObjectContext) {
for item in currentSelected(in: context) {
item.isSelected = false
}
}
func selectProduct(_ product: Product) {
guard let context = product.managedObjectContext else {
assertionFailure("broken !")
return
}
clearSelection(in: context)
product.isSelected = true
}
func currentSelected(in context: NSManagedObjectContext) -> [Product] {
let request = NSFetchRequest<Product>(entityName: Product.entity().name!)
let predicate = NSPredicate(format: "isSelected == YES")
request.predicate = predicate
do {
let result = try context.fetch(request)
return result
} catch {
print("fetch error =",error)
return []
}
}
}
which you can then use to select your desired product.
SelectionHandler().selectProduct(product)
As it stands your NavigationLink will do nothing because the parent list is not held in a NavigationView so you'll need to change the body of ViewList to look like this.
var body: some View {
NavigationView {
VStack {
HStack {
TextField("Create product with name", text: $newName)
Button(action: {
self.add()
self.newName = ""
})
{ Image(systemName: "plus") }
}
.padding()
List {
ForEach(list, id: \.self) { product in
ViewItem(product: product)
}
}
}
}
}
and in ViewItem , Product should be an ObservedObject so that changes are detected in the managedObject.
struct ViewItem: View {
#ObservedObject var product: Product
#State var refresh: Bool = false
var checkmarkImage: some View {
return Group {
if self.product.isSelected {
Image(systemName: "checkmark")
} else {
Image(systemName: "checkmark").colorInvert()
}
}
}
var body: some View {
NavigationLink(destination: ViewDetail(product: product, refresh: $refresh)) {
HStack {
checkmarkImage
Text(product.name ?? "wat")
}
}
}
}
The original Button won't play with the NavigationLink but you can simply apply the selection to onAppear in ViewDetail
struct ViewDetail: View {
#ObservedObject var product: Product
#Binding var refresh: Bool
var body: some View {
VStack {
Text("Hello, World!")
Text("Product is \(product.name ?? "wat")")
}
.onAppear {
SelectionHandler().selectProduct(self.product)
}
}
}

Inactive navigation buttons

I'm trying to make a simple Master-Detail-FileViewer app. In the last FileViewer view I want to have a button, which has an option to make the file favourite (every file has an "id" string, which is appended to an Environment object). When you favour them, this object is shown at the master view for quick access for the user, linking to the FileViewer view. However, when the user taps and goes there, the button is inactive - you cannot tap it and it gets black from blue. If you want to remove them from favourites, you can't.
I'd really appreciate to tell me what is wrong and how to make the button active. No error is shown and the app doesn't crash. It just doesn't work.
Thanks in advance!
The files are either "judgement" and "secondary", both have id and title properties. The second picture is the problematic one.
import SwiftUI
struct ContentView: View {
#EnvironmentObject var favouriteList: FavouritesList
var body: some View {
NavigationView {
List {
NavigationLink(destination: JudgementsView()) {
Text("Judgements")
}
NavigationLink(destination: SecondaryView()) {
Text("Secondary acts")
}
ScrollView(.horizontal, showsIndicators: false) {
VStack {
if favouriteList.items.isEmpty {
Text("Nothing favoured")
} else {
ForEach(favouriteList.items, id: \.self) { id in
VStack {
HStack {
ForEach(judgementsTAXraw.filter {
$0.id == id
}) { judgement in
NavigationLink(destination: FileViewer(file: judgement.id)) {
Text(judgement.title).padding()
}
}
}
HStack {
ForEach(secondaryTAXraw.filter {
$0.id == id
}) { secondary in
NavigationLink(destination: FileViewer(file: secondary.id)) {
Text(secondary.title).padding()
}
}
}
}
}
}
}
}
}
.navigationBarTitle(Text("Test"))
}
}
}
struct JudgementsView: View {
var body: some View {
List(judgementsTAXraw, id: \.id) { judgement in
NavigationLink(destination: FileViewer(file: judgement.id)) {
Text(judgement.title)
}
}
}
}
struct SecondaryView: View {
var body: some View {
List(secondaryTAXraw, id: \.id) { secondary in
NavigationLink(destination: FileViewer(file: secondary.id)) {
Text(secondary.title)
}
}
}
}
struct FileViewer: View {
var file: String
#State private var showCopySheet = false
#EnvironmentObject var favouriteList: FavouritesList
var body: some View {
Text(file)
.navigationBarTitle(Text(""), displayMode: .inline)
.navigationBarItems(trailing:
Button(action: {
self.showCopySheet = true
}) {
Image(systemName: "doc.on.doc").frame(minWidth: 40)
}.actionSheet(isPresented: $showCopySheet) {
ActionSheet(title: Text("What do you want to do?"), buttons: [
.destructive(Text("favText"), action: {
if let index = self.favouriteList.items.firstIndex(of: self.file) {
self.favouriteList.items.remove(at: index)
} else {
self.favouriteList.items.append(self.file)
}
}),
.cancel()
])
}
)
}
}
Aaaand in a separate file is the object:
import Foundation
class FavouritesList: ObservableObject {
#Published var items = [String]() {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "FavouredItems")
}
}
}
init() {
if let items = UserDefaults.standard.data(forKey: "FavouredItems") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([String].self, from: items) {
self.items = decoded
return
}
}
self.items = []
}
}