SwiftUI preference, transformPreference, onPreferenceChanged - swiftui

How to pass several preferences for given preference key?
var body: some View {
MyTabView {
Page1()
.preference(key: TabItemPreferenceKey.self, value: [TabItemPreference(tag: 0, item: AnyView(Text("Tab 1")) ) ] )
Page2()
.preference(key: TabItemPreferenceKey.self, value: [TabItemPreference(tag: 1, item: AnyView(Text("Tab 12")) ) ] )
Then in MyTabView I am trying to access this preferences but there is only first preference available
.onPreferenceChange(TabItemPreferenceKey.self) { preferences in
preferences.forEach { p in
self.items.tabItems.append((tag: p.tag, tab: p.item))
}
}

You can use
.transformPreference( _, _)
to combine multiple preferences

After some experimental debugging I've found the source of the problem. I've used .preference(key:) call on PageX() views inside MyTabView, but this views were conditionally displayed on screen! So .preference(key:) registers this preference only for displayed views.
I make a workaround to this issue using .hidden(). I only have concerns about performance as it would be better to display only needed page not add all pages at once and then hide them. But for now I do not know any other workaround.
Here is How I display this views to which I am attaching preferences:
var body: some View {
ForEach(0..<childs.count, id: \.self) { i in
Group {
if i == self.model.selection {
self.childs[i]
} else {
self.childs[i].hidden()
}
}
}
}

Related

Vertical SwiftUIPager breaks when adding content on the start of the list, on demand

I have an app, that shows posts on a vertical SwiftUIPager. Each page fills the whole screen.
So, when the app launches, I fetch the 10 most recent posts and display them.
As soon as those posts are fetched, I start listening for new posts. (see code below) Whenever that callback gets triggered, a new post is created. I take it and place it on the top of my list.
The thing is when I scroll to find the new post, its views get mixed up with the views of the next post.
Here's what I mean:
Before the new post, I have the one below
https://imgur.com/a/ZmMzfvb
And then, a new post is added to the top
https://imgur.com/a/PJ0trSF
As you'll notice the image seems to be the same, but it shouldn’t! If I scroll for a while and then go back up, the new post will be fixed and display the proper image. (I'm using SDWebImageSwiftUI for async images, but I don't think it matters... I also used Apple's AsyncImage, with the same results)
Here's my feed view model:
#Published var feedPage: Page = .first()
#Published var feedItems = Array(0..<2)
var posts = [Post]()
...
private func subscribeToNewPosts() {
postsService.subscribeToNewPosts() { [weak self] post in
self?.posts.insert(post, at: 0)
DispatchQueue.main.async {
self?.feedItems = Array(0..<(self?.posts.count ?? 1))
}
}
}
And here's my feed view:
private struct FeedPageView: View {
#EnvironmentObject private var viewModel: FeedView.ViewModel
var body: some View {
ZStack {
VStack {
Pager(page: viewModel.feedPage,
data: viewModel.feedItems,
id: \.self,
content: { index in
if index == 0 {
HomeCameraView()
.background(.black)
} else {
PostView(post: viewModel.posts[index - 1])
}
})
.vertical()
.sensitivity(.custom(0.1))
.onPageWillChange { index in
viewModel.willChangeVerticalPage(index: index)
}
}
}
}
}
Any idea what I'm doing wrong?

How to respond to hover / click event on AttributedString in SwiftUI

I am using AttributedString in SwiftUI (Mac application) to customize the appearance of portions of a long string. I'm displaying the text formatted successfully and it appears correct.
My code looks like this:
struct TextView: View {
var body: some View {
ScrollView {
Text(tag())
}.padding()
}
func tag() -> AttributedString {
// code which creates the attributed string and applies formatting to various locations
}
}
At this point I want to add "touch points" ("interactive points") to the text (imagine hyperlinks) which will provide additional information when particular locations (pieces of text) are interacted with.
Ive seen some similar questions describing usage (or combinations) of NSTextAttachment , NSAttributedStringKey.link , UITextViewDelegate
see:
NSAttributedString click event in UILabel using Swift
but this isn't (or at least not obvious) the idiomatic "SwiftUI" way and seems cumbersome.
I would want to tag the string with the formatting while adding the "Attachment" which can be recognized in the view event handler:
func tag() -> AttributedString {
// loose for this example
var attributedString = AttributedString("My string which is very long")
for range in getRangesOfAttributes {
attributedString[range].foregroundColor = getRandomColor()
attributedString[range].attachment = Attachment() <<<<<<< this is missing, how do I tag this portion and recognize when it got interacted with in the View
}
}
func getRangesOfAttributes() -> ClosedRange<AttributedString.Index> {
... returns a bunch of ranges which need to be tagged
}
// the view can now do something once the attachment is clicked
var body: View {
Text(tag())
.onClickOfAttachment(...) // <<<< This is contrived, how can I do this?
}

Changing swipeActions dynamically in SwiftUI

I am trying to change the swipeAction from "Paid" to "UnPaid" based on payment status and somehow seems to be failing. Error: "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
Appreciate any help
struct ContentView: View {
var data: [Data] = [data1, data2, data3, data4]
#State var swipeLabel = true
var body: some View {
let grouped = groupByDate(data)
List {
ForEach(Array(grouped.keys).sorted(by: >), id: \.self) { date in
let studentsDateWise = grouped[date]!
Section(header:Text(date, style: .date)) {
ForEach(studentsDateWise, id:\.self) { item in
HStack {
Text(item.name)
padding()
Text(item.date, style: .time)
if(item.paymentStatus == false) {
Image(systemName: "person.fill.questionmark")
.foregroundColor(Color.red)
} else {
Image(systemName: "banknote")
.foregroundColor(Color.green)
}
} // HStack ends here
.swipeActions() {
if(item.paymentStatus) {
Button("Paid"){}
} else {
Button("UnPaid"){}
}
}
} // ForEach ends here...
} // section ends here
} // ForEach ends here
} // List ends here
} // var ends here
}
The body func shouldn't do any grouping or sorting. You need to prepare your data first into properties and read from those in body, e.g. in an onAppear block. Also if your Data is a struct you can't use id: \.self you need to either specify a unique identifier property on the data id:\.myUniqueID or implement the Indentifiable protocol by either having an id property or an id getter that computes a unique identifier from other properties.
I would suggest separating all this code into small Views with a small body that only uses one or a two properties. Work from bottom up. Then eventually with one View works on an array of dates and another on an array of items that contains the small Views made earlier.
You should probably also learn that if and foreach in body are not like normal code, those are converted into special Views. Worth watching Apple's video Demystify SwiftUI to learn about structural identity.

SwiftUI - Fatal error: each layout item may only occur once: file SwiftUI, line 0

OK before anymore marks this as duplicate, I have looked at posts similar to this on StackOverFlow and even the Apple forums, and implemented all recommended solutions, yet the problem persists. I've using a LazyVGrid. My code is as follows:
This is my custom scroll view that allows for pagination once the user reaches the end of the scroll:
if !quickSearchViewModel.isLoading && !quickSearchViewModel.search_result.isEmpty {
DataFetchingScrollView(.vertical, alignment: .center, onOffsetChange: { (off, height) in
offset = off
heightMinusOffset = height
if heightMinusOffset <= UIScreen.main.bounds.height &&
!quickSearchViewModel.search_result.isEmpty {
quickSearchViewModel.paginate_searchterm {
print("Paginated")
} onError: { (error) in
print(error)
}
}
}) {
createGrid()
.id(UUID())
}
And these are my two functions to create the grid and the view inside the grid:
private func createGrid() -> some View {
LazyVGrid(columns: columns) {
ForEach(quickSearchViewModel.search_result, id: \.product_uid) { product in
createProductItemView(product)
.id(product.product_uid)
}
}
}
private func createProductItemView(_ product: ProductModel) -> some View {
ProductItemView(product: product)
.id(product.product_uid)
}
Yes, I know I have spammed the id, but I've added '.id' to all views indivdually and the problem persists. It's as soon as I hit search, the content loads and appears in the grid and thats when my application crashes.
Edit - the product.product_uid is an auto-id generated by Firebase. I am literally using this same method in other views, and some work without issues, others may have a little bug.
Found this issue, it was because I was using orderBy when querying data.

SwiftUI GKLeaderboard loadEntries

I would like to add leaderboards to my SwiftUI app.
I can't find any examples of using loadEntries to load leaderboard values.
I tried the following...
let leaderBoard: GKLeaderboard = GKLeaderboard()
leaderBoard.identifier = "YOUR_LEADERBOARD_ID_HERE"
leaderBoard.timeScope = .allTime
leaderBoard.loadScores { (scores, error) in ...
This results in the following warnings:
'identifier' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'timeScope' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'loadScores(completionHandler:)' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler:.
using loadEntriesForPlayerScope results in the following warning:
'loadEntriesForPlayerScope(_:timeScope:range:completionHandler:)' has
been renamed to 'loadEntries(for:timeScope:range:completionHandler:)'
Using loadEntries I don't know how to specify the leaderboard identifier.
Here is simple demo of possible approach - put everything in view model and load scores on view appear.
import GameKit
class BoardModel: ObservableObject {
private var board: GKLeaderboard?
#Published var localPlayerScore: GKLeaderboard.Entry?
#Published var topScores: [GKLeaderboard.Entry]?
func load() {
if nil == board {
GKLeaderboard.loadLeaderboards(IDs: ["YOUR_LEADERBOARD_ID_HERE"]) { [weak self] (boards, error) in
self?.board = boards?.first
self?.updateScores()
}
} else {
self.updateScores()
}
}
func updateScores() {
board?.loadEntries(for: .global, timeScope: .allTime, range: NSRange(location: 1, length: 10),
completionHandler: { [weak self] (local, entries, count, error) in
DispatchQueue.main.async {
self?.localPlayerScore = local
self?.topScores = entries
}
})
}
}
struct DemoGameboardview: View {
#StateObject var vm = BoardModel()
var body: some View {
List {
ForEach(vm.topScores ?? [], id: \.self) { item in
HStack {
Text(item.player.displayName)
Spacer()
Text(item.formattedScore)
}
}
}
.onAppear {
vm.load()
}
}
}
I might be stating the obvious but have you looked at the WWDC20 videos?
Usually when there are big changes like this they cover it during WWDC that year.
Tap into Game Center: Leaderboards, Achievements, and Multiplayer
Tap into Game Center: Dashboard, Access Point, and Profile
I haven't looked at the videos but the documentation eludes that identifier might be replaced by var baseLeaderboardID: String