I have an app that fetches a list of items with images URL's from remote API, and then it has to fetch an image per item from given url inside that item's position.
The problem is that when scrolling up and down and thus removing list items from view and moving them back into view they do show up. However on initial load they stay in "loading" status forever until moved out and in.
My code:
import SwiftUI
struct ContentView: View {
#EnvironmentObject var artObjectStore: ArtObjectStore
#State private var pageCount = 1
#State private var tappedLink: String? = nil
#Environment(\.imageCache) var cache: ImageCache
var body: some View {
NavigationView {
Form {
Section(header: Text("Art")) {
List {
ForEach(artObjectStore.artObjects, id: \.self) { artObject in
self.link(for: artObject)
}
Button(action: loadMore) {
Text("")
}
.onAppear {
DispatchQueue.global(qos: .background).asyncAfter(deadline: DispatchTime(uptimeNanoseconds: 10)) {
self.loadMore()
}
}
}
}
}
.navigationBarTitle("Art objects")
}
.onAppear(perform: loadMore)
}
func loadMore() {
pageCount += 1
artObjectStore.loadMore(pageCount)
}
private func link(for artObject: ArtObject) -> some View {
let selection = Binding(get: { self.tappedLink },
set: {
UIApplication.shared.endEditing()
self.tappedLink = $0
})
return NavigationLink(destination: DetailView(artObject: artObject, cache: self.cache),
tag: artObject.id,
selection: selection) {
HStack(alignment: .center) {
VStack(alignment: .leading){
Text("\(artObject.title)").font(.system(size: 12))
Text("\(artObject.principalOrFirstMaker)").font(.system(size: 9)).foregroundColor(.gray)
}
Spacer()
AsyncImage(
url: URL(string: artObject.headerImage.url)!,
cache: self.cache,
width: 200,
height: 50
)
}
}
}
}
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Image container:
import SwiftUI
import Combine
import Foundation
class ImageLoader: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
private var cancellable: AnyCancellable?
#Published var image: UIImage? {
willSet {
objectWillChange.send()
}
}
private let url: URL
private var cache: ImageCache?
init(url: URL, cache: ImageCache? = nil) {
self.url = url
self.cache = cache
}
deinit {
cancellable?.cancel()
}
private func cache(_ image: UIImage?) {
image.map { cache?[url] = $0 }
}
func load() {
if let image = cache?[url] {
self.image = image
return
}
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.handleEvents(receiveOutput: { [weak self] in self?.cache($0) })
.receive(on: DispatchQueue.main)
.assign(to: \.image, on: self)
}
func cancel() {
cancellable?.cancel()
}
}
struct AsyncImage: View {
#ObservedObject private var loader: ImageLoader
private let width: CGFloat?
private let height: CGFloat?
#State var spin = false
init(url: URL, cache: ImageCache? = nil, width: CGFloat? = nil, height: CGFloat? = nil) {
loader = ImageLoader(url: url, cache: cache)
self.width = width
self.height = height
}
var body: some View {
image
.onAppear(perform: loader.load)
.onDisappear(perform: loader.cancel)
}
private var image: some View {
Group {
if loader.image != nil {
Image(uiImage: loader.image!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: width, height: height)
} else {
Image("loadingCircle")
.resizable()
.frame(width: 20, height: 20)
.rotationEffect(.degrees(spin ? 360 : 0))
.animation(Animation.linear(duration: 0.8).repeatForever(autoreverses: false))
.onAppear() {
self.spin.toggle()
}
}
}
}
}
protocol ImageCache {
subscript(_ url: URL) -> UIImage? { get set }
}
struct TemporaryImageCache: ImageCache {
private let cache = NSCache<NSURL, UIImage>()
subscript(_ key: URL) -> UIImage? {
get { cache.object(forKey: key as NSURL) }
set { newValue == nil ? cache.removeObject(forKey: key as NSURL) : cache.setObject(newValue!, forKey: key as NSURL) }
}
}
struct ImageCacheKey: EnvironmentKey {
static let defaultValue: ImageCache = TemporaryImageCache()
}
extension EnvironmentValues {
var imageCache: ImageCache {
get { self[ImageCacheKey.self] }
set { self[ImageCacheKey.self] = newValue }
}
}
I did try to add willSet on the image, that doesn't seem to work. Can you help me?
All below tested with Xcode 11.4 / iOS 13.4
Modified worked AsyncImage
I changed Group to VStack and it started updating
private var image: some View {
VStack { // << here !!
if loader.image != nil {
Modified worked image loader
class ImageLoader: ObservableObject {
#Published var image: UIImage?
private var cancellable: AnyCancellable?
private let url: URL
private var cache: ImageCache?
init(url: URL, cache: ImageCache? = nil) {
self.url = url
self.cache = cache
}
deinit {
cancellable?.cancel()
}
private func cache(_ image: UIImage?) {
self.image = image
image.map { cache?[url] = $0 }
}
func load() {
if let image = cache?[url] {
self.image = image
return
}
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.receive(on: DispatchQueue.main)
.replaceError(with: nil)
.sink(receiveValue: { [weak self] in self?.cache($0) })
}
func cancel() {
cancellable?.cancel()
}
}
Related
How can I make a slider when my Data is coming from API? I am using
this(below code) for static images work fine but whenever I try to
use API data then my code does not work.
How to Set the Marquee in this images.
This is My code
public struct MagazineModel: Decodable {
public let magzineBanners: [MagzineBanner]
}
public struct MagzineBanner: Decodable, Identifiable {
public let id: Int
public let url: String
}
This is My View Model
//View Model for Magazines and showing Details
class MagazineBannerVM: ObservableObject{
#Published var datas = [MagzineBanner]()
let url = "ApiUrl"
init() {
getData(url: url)
}
func getData(url: String) {
guard let url = URL(string: url) else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
if let data = data {
do {
let results = try JSONDecoder().decode(MagazineModel.self, from: data)
DispatchQueue.main.async {
self.datas = results.magzineBanners
}
}
catch {
print(error)
}
}
}.resume()
}
}
struct MagazineBannerView: View{
#ObservedObject var list = MagazineBannerVM()
public let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
#State var currentIndex = 0
#State var totalImages = 2
var body: some View{
ScrollView(.horizontal) {
GeometryReader { proxy in
TabView(selection: $currentIndex) {
HStack{
ForEach(list.datas, id: \.id){ item in
Group{
AsyncImage(url: URL(string: item.url)){ image in
image
.resizable()
.frame(width:UIScreen.main.bounds.width, height: 122)
}placeholder: {
Image("logo_gray").resizable()
.frame(width:UIScreen.main.bounds.width, height: 122)
}
}
}
}
}
.tabViewStyle(PageTabViewStyle())
.onReceive(timer, perform: { _ in
withAnimation{
currentIndex = currentIndex < totalImages ? currentIndex + 1: 0
}
})
}
}
}
}
I want to change images after every 2 seconds and every images has
full width as the screen width
And it is showing the half of screen width and showing both images in
single view
I have a reminder app that I am trying to implement persistent data in but whenever I close the app no data is saved. I know how to make it work with a normal MVC but I would like to get it working with the view model that I have.
I think I know what needs to change to fix the problem but I am not sure how to get to the solution. I am pretty sure that in the ReminderApp file under the NavigationView where it says HomeViewModel(reminds: store.reminds) I think that the store.reminds part needs to be binded to with a $ at the beginning but when I try doing that it doesn't work and instead says that HomeViewModel reminds property expects Reminder instead of Binding.
ReminderStore loads and saves the reminders to a file with the reminders and HomeViewModel contains the reminders array and appends a reminder to the array when a user adds a new reminder.
If anyone knows how to get this working that would be great since I have been stuck on this. My minimal reproducable example code is below.
RemindersApp
import SwiftUI
#main
struct RemindersApp: App {
#StateObject private var store = ReminderStore()
var body: some Scene {
WindowGroup {
NavigationView {
HomeView(homeVM: HomeViewModel(reminds: store.reminds)) {
ReminderStore.save(reminds: store.reminds) { result in
if case .failure(let error) = result {
fatalError(error.localizedDescription)
}
}
}
.navigationBarHidden(true)
}
.onAppear {
ReminderStore.load { result in
switch result {
case .failure(let error):
fatalError(error.localizedDescription)
case .success(let reminds):
store.reminds = reminds
}
}
}
}
}
}
HomeView
import SwiftUI
struct HomeView: View {
#StateObject var homeVM: HomeViewModel
#Environment(\.scenePhase) private var scenePhase
#State var addView = false
let saveAction: ()->Void
var body: some View {
VStack {
List {
ForEach($homeVM.reminds) { $remind in
Text(remind.title)
}
}
}
.safeAreaInset(edge: .top) {
HStack {
Text("Reminders")
.font(.title)
.padding()
Spacer()
Button(action: {
addView.toggle()
}) {
Image(systemName: "plus")
.padding()
.font(.title2)
}
.sheet(isPresented: $addView) {
NavigationView {
VStack {
Form {
TextField("Title", text: $homeVM.newRemindData.title)
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Dismiss") {
homeVM.newRemindData = Reminder.Data()
addView.toggle()
}
}
ToolbarItem(placement: .principal) {
Text("New Reminder")
.font(.title3)
}
ToolbarItem(placement: .confirmationAction) {
Button("Add") {
homeVM.addRemindData(remindData: homeVM.newRemindData)
addView.toggle()
}
}
}
}
}
.onChange(of: scenePhase) { phase in
if phase == .inactive { saveAction() }
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
HomeView(homeVM: HomeViewModel(reminds: Reminder.sampleReminders), saveAction: {})
}
}
ReminderStore
import Foundation
import SwiftUI
class ReminderStore: ObservableObject {
#Published var reminds: [Reminder] = []
private static func fileURL() throws -> URL {
try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
.appendingPathComponent("reminds.data")
}
static func load(completion: #escaping (Result<[Reminder], Error>) -> Void) {
DispatchQueue.global(qos: .background).async {
do {
let fileURL = try fileURL()
guard let file = try? FileHandle(forReadingFrom: fileURL) else {
DispatchQueue.main.async {
completion(.success([]))
}
return
}
let reminds = try JSONDecoder().decode([Reminder].self, from: file.availableData)
DispatchQueue.main.async {
completion(.success(reminds))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
static func save(reminds: [Reminder], completion: #escaping (Result<Int, Error>) -> Void) {
do {
let data = try JSONEncoder().encode(reminds)
let outfile = try fileURL()
try data.write(to: outfile)
DispatchQueue.main.async {
completion(.success(reminds.count))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
HomeViewModel
import Foundation
class HomeViewModel: ObservableObject {
#Published var reminds: [Reminder]
#Published var newRemindData = Reminder.Data()
init(reminds: [Reminder]) {
self.reminds = reminds
}
func addRemindData(remindData: Reminder.Data) {
let newRemind = Reminder(data: remindData)
reminds.append(newRemind)
newRemindData = Reminder.Data()
}
}
Reminder
import Foundation
struct Reminder: Identifiable, Codable {
var title: String
let id: UUID
init(title: String, id: UUID = UUID()) {
self.title = title
self.id = id
}
}
extension Reminder {
struct Data {
var title: String = ""
var id: UUID = UUID()
}
var data: Data {
Data(title: title)
}
mutating func update(from data: Data) {
title = data.title
}
init(data: Data) {
title = data.title
id = UUID()
}
}
extension Reminder {
static var sampleReminders = [
Reminder(title: "Reminder1"),
Reminder(title: "Reminder2"),
Reminder(title: "Reminder3")
]
}
The reason you are struggeling here is because you try to have multiple Source of truth.
documentation on dataflow in SwiftUI
You should move the code from HomeViewModel to your ReminderStore and change the static functions to instance functions. This would keep your logic in one place.
You can pass your ReminderStore to your HomeView as an #EnvironmentObject
This would simplify your code to:
class ReminderStore: ObservableObject {
#Published var reminds: [Reminder] = []
#Published var newRemindData = Reminder.Data()
private func fileURL() throws -> URL {
try FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
.appendingPathComponent("reminds.data")
}
func load() {
DispatchQueue.global(qos: .background).async {
do {
let fileURL = try self.fileURL()
guard let file = try? FileHandle(forReadingFrom: fileURL) else {
return
}
let reminds = try JSONDecoder().decode([Reminder].self, from: file.availableData)
DispatchQueue.main.async {
self.reminds = reminds
}
} catch {
DispatchQueue.main.async {
fatalError(error.localizedDescription)
}
}
}
}
func save() {
do {
let data = try JSONEncoder().encode(reminds)
let outfile = try fileURL()
try data.write(to: outfile)
} catch {
fatalError(error.localizedDescription)
}
}
func addRemindData() {
let newRemind = Reminder(data: newRemindData)
reminds.append(newRemind)
newRemindData = Reminder.Data()
}
}
struct RemindersApp: App {
#StateObject private var store = ReminderStore()
var body: some Scene {
WindowGroup {
NavigationView {
HomeView() {
store.save()
}
.navigationBarHidden(true)
.environmentObject(store)
}
.onAppear {
store.load()
}
}
}
}
struct HomeView: View {
#Environment(\.scenePhase) private var scenePhase
#EnvironmentObject private var store: ReminderStore
#State var addView = false
let saveAction: ()->Void
var body: some View {
VStack {
List {
ForEach(store.reminds) { remind in
Text(remind.title)
}
}
}
.safeAreaInset(edge: .top) {
HStack {
Text("Reminders")
.font(.title)
.padding()
Spacer()
Button(action: {
addView.toggle()
}) {
Image(systemName: "plus")
.padding()
.font(.title2)
}
.sheet(isPresented: $addView) {
NavigationView {
VStack {
Form {
TextField("Title", text: $store.newRemindData.title)
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Dismiss") {
store.newRemindData = Reminder.Data()
addView.toggle()
}
}
ToolbarItem(placement: .principal) {
Text("New Reminder")
.font(.title3)
}
ToolbarItem(placement: .confirmationAction) {
Button("Add") {
store.addRemindData()
addView.toggle()
}
}
}
}
}
.onChange(of: scenePhase) { phase in
if phase == .inactive { saveAction() }
}
}
}
}
}
An issue I would recommend solving:
Naming a type after something that´s allready taken by Swift is a bad idea. You should rename your Data struct to something different.
ReminderStore.save isn't invoking in time.
By the time it invokes it doesn't have/get the reminder data.
That's the first thing I would make sure gets done. You may end up running into other issues afterward, but I would personally focus on that first.
I am trying to build a simple horizontal list of images. The images are loaded asynchronously, nothing fancy so far.
The main issue is that when scrolling and loading new images, the memory increases and never gets released. In case the images are really big, or the list of items very long, the app can crash while scrolling.
Shouldn't the LazyHStack do some memory management of views? I can not imagine this is not resolved so far, since it seems to me a very basic "feature".
The code (nothing special):
struct ThumbnailsCollectionView: View {
var imageService = ImageService.shared
#Binding var selectedIndex: Int
#State var image: UIImage?
var body: some View {
ScrollView([.horizontal]){
ScrollViewReader { reader in
LazyHStack(alignment: .bottom, spacing: 5){
ForEach((0...imageService.imageCount()-1), id: \.self) { index in
ThumbnailImageView(index: index)
}
}
}.padding(.vertical, 10)
}
}
}
struct ThumbnailImageView: View {
var imageService = ImageService.shared
var index: Int
#State var image: UIImage? = nil
var body: some View {
return Button(action: {
}) {
if let uiImage = image {
Image(uiImage: uiImage).resizable()
.aspectRatio(contentMode: .fit)
.zIndex(5)
}
else {
Rectangle().frame(width: 50, height: 50, alignment: .center)
}
}
.onAppear() {
imageService.thumbnail(at: index) { (image) in
self.image = image
}
}
}
}
ImageServiceCache
class ImageService {
static let shared = ImageService()
var thumbnailCache: NSCache<NSNumber, UIImage> = {
let cache = NSCache<NSNumber,UIImage>()
cache.countLimit = 10
return cache
}()
var urls:[URL] = {
var urls = [URL]()
for i in 0...100 {
let index = i % 4
urls.append(Bundle.main.url(forResource: "\(index)", withExtension: "jpg")!)
}
return urls
}()
func imageCount() -> Int {
return urls.count
}
func thumbnail(at index: Int, completion:#escaping (UIImage)->()) {
if let imageFromCache = thumbnailCache.object(forKey: NSNumber(integerLiteral: index)) {
return completion(imageFromCache)
}
DispatchQueue.global().async {
let image = UIImage(contentsOfFile: self.urls[index].path)!
self.thumbnailCache.setObject(image, forKey: NSNumber(integerLiteral: index))
DispatchQueue.main.async {
completion(image)
}
}
}
}
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()
}
}
}
I am trying to drag and drop between to Lists.
What I have tried:
I have found a solution doing it in UIKIt and the using UIViewControllerRepresentable. But that is not what i want.
The other solution was using .onDrag {} on list, but that worked on iPad and didn't work on iPhone.
How to move items between two Lists on iPhone?
check this out:
BUT -> as you wrote, it works NOT with list (just on iPad), with VStack
you can drag from left to right or right to left.
import SwiftUI
struct BookmarksList2: View {
#State private var links: [URL] = [
URL(string: "https://www.apple.com")!
]
var body: some View {
VStack {
ForEach(links, id: \.self) { url in
Text(url.absoluteString)
.onDrag {
NSItemProvider(object: url as NSURL)
}
}
.onInsert(of: ["public.url"], perform: drop)
.onDrop(
of: ["public.url"],
delegate: BookmarksDropDelegate(bookmarks: $links)
)
}
.navigationBarTitle("Bookmarks")
}
private func drop(at index: Int, _ items: [NSItemProvider]) {
for item in items {
_ = item.loadObject(ofClass: URL.self) { url, _ in
DispatchQueue.main.async {
url.map { self.links.insert($0, at: index) }
}
}
}
}
}
struct BookmarksList3: View {
#State private var links: [URL] = [
URL(string: "https://www.google.com")!
]
var body: some View {
VStack {
ForEach(links, id: \.self) { url in
Text(url.absoluteString)
.onDrag {
NSItemProvider(object: url as NSURL)
}
}
.onInsert(of: ["public.url"], perform: drop)
.onDrop(
of: ["public.url"],
delegate: BookmarksDropDelegate(bookmarks: $links)
)
}
.navigationBarTitle("Bookmarks")
}
private func drop(at index: Int, _ items: [NSItemProvider]) {
for item in items {
_ = item.loadObject(ofClass: URL.self) { url, _ in
DispatchQueue.main.async {
url.map { self.links.insert($0, at: index) }
}
}
}
}
}
struct BookmarksDropDelegate: DropDelegate {
#Binding var bookmarks: [URL]
func performDrop(info: DropInfo) -> Bool {
guard info.hasItemsConforming(to: ["public.url"]) else {
return false
}
let items = info.itemProviders(for: ["public.url"])
for item in items {
_ = item.loadObject(ofClass: URL.self) { url, _ in
if let url = url {
DispatchQueue.main.async {
self.bookmarks.insert(url, at: 0)
}
}
}
}
return true
}
}
struct ContentView : View {
var body: some View {
HStack {
BookmarksList2()
Divider()
BookmarksList3()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}