How to load next Image while user is looking at first image in the TabView pages carousel with AsyncImageView In SwiftUI? - swift

I have an images carousel that fetching heavy images from a few URLs and displaying asynchronously when user is going to the next page. And the problem is that user need to see a waiting wheel and wait to see the next image.
So the ideal case would be to eliminate the waiting wheel by loading the next image while user still seeing the fist image and I don't know how to do it properly.
If you know the solution please let me know, I will mean a lot to me.
Here's the code:
struct ContentView: View {
#State private var selectedImageToSee: Int = 0
private let imagesUrl: [String] = [
"https://images.unsplash.com/photo-1633125195723-04860dde4545?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzNDIyMDUyMQ&ixlib=rb-1.2.1&q=80&w=3024",
"https://images.unsplash.com/photo-1633090332452-532d6b39422a?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTYzNDIyMDI1Ng&ixlib=rb-1.2.1&q=80&w=3024",
"https://images.unsplash.com/photo-1591667954789-c25d5affec59?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLGNhcnN8fHx8fHwxNjM0MjIwNzU1&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=3024",
"https://images.unsplash.com/photo-1580316576539-aee1337e80e8?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLGNhcnN8fHx8fHwxNjM0MjIwNzk5&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=3024",
"https://images.unsplash.com/photo-1484976063837-eab657a7aca7?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=4032&ixid=MnwxfDB8MXxyYW5kb218MHx8bmF0dXJlLGNhcnN8fHx8fHwxNjM0MjIwNjky&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=3024"
]
var body: some View {
ImagesCaruselView(selectedImageToSee: $selectedImageToSee, urls: imagesUrl)
}
}
struct ImagesCaruselView: View {
#Binding var selectedImageToSee: Int
let urls: [String]
var body: some View {
TabView(selection: $selectedImageToSee) {
ForEach(0..<urls.count, id: \.self) { url in
AsyncImageView(
url: URL(string: urls[url])!,
placeholder: { ProgressView().scaleEffect(1.5).progressViewStyle(CircularProgressViewStyle(tint: .gray)) },
image: { Image(uiImage: $0) }
)
.padding()
}
}
.tabViewStyle(.page)
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
}
struct AsyncImageView<Placeholder: View>: View {
#StateObject private var loader: ImageLoader
private let placeholder: Placeholder
private let image: (UIImage) -> Image
init(
url: URL,
#ViewBuilder placeholder: () -> Placeholder,
#ViewBuilder image: #escaping (UIImage) -> Image = Image.init(uiImage:)
) {
self.placeholder = placeholder()
self.image = image
_loader = StateObject(wrappedValue: ImageLoader(url: url, cache: Environment(\.imageCache).wrappedValue))
}
var body: some View {
content
.onAppear(perform: loader.load)
}
private var content: some View {
Group {
if loader.image != nil {
image(loader.image!)
.resizable()
.aspectRatio(contentMode: .fit)
} else {
placeholder
}
}
}
}
protocol ImageCache {
subscript(_ url: URL) -> UIImage? { get set }
}
struct TemporaryImageCache: ImageCache {
private let cache: NSCache<NSURL, UIImage> = {
let cache = NSCache<NSURL, UIImage>()
cache.countLimit = 100 // 100 items
cache.totalCostLimit = 1024 * 1024 * 200 // 200 MB
return cache
}()
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) }
}
}
import Combine
class ImageLoader: ObservableObject {
#Published var image: UIImage?
private(set) var isLoading = false
private let url: URL
private var cache: ImageCache?
private var cancellable: AnyCancellable?
private static let imageProcessingQueue = DispatchQueue(label: "image-processing")
init(url: URL, cache: ImageCache? = nil) {
self.url = url
self.cache = cache
}
deinit {
cancel()
}
func load() {
guard !isLoading else { return }
if let image = cache?[url] {
self.image = image
return
}
#warning("Caching the image are disabled")
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { UIImage(data: $0.data) }
.replaceError(with: nil)
.handleEvents(receiveSubscription: { [weak self] _ in self?.onStart() },
// receiveOutput: { [weak self] in self?.cache($0) },
receiveCompletion: { [weak self] _ in self?.onFinish() },
receiveCancel: { [weak self] in self?.onFinish() })
.subscribe(on: Self.imageProcessingQueue)
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.image = $0 }
}
func cancel() {
cancellable?.cancel()
}
private func onStart() {
isLoading = true
}
private func onFinish() {
isLoading = false
}
private func cache(_ image: UIImage?) {
image.map { cache?[url] = $0 }
}
}
import SwiftUI
struct ImageCacheKey: EnvironmentKey {
static let defaultValue: ImageCache = TemporaryImageCache()
}
extension EnvironmentValues {
var imageCache: ImageCache {
get { self[ImageCacheKey.self] }
set { self[ImageCacheKey.self] = newValue }
}
}

Related

SwiftUI ObservableObject with #Published array

The UI will update when the array is reassigned to a whole new array, but if I perform a remove on the array the UI will not update.
I was thinking maybe since it was still pointing to the same place in memory the UI wouldn’t be notified of the change. However, I added a didSet property observer to print every time the array was changed and the print statement executed on the remove.
I’m at a loss for how to debug further. Any help would be appreciated!
Code:
Here are the views. There is a movie list view which displays a grid of movie views
struct MovieListView: View {
#ObservedObject var viewModel: MovieListViewModel
init(viewModel: MovieListViewModel) {
self.viewModel = viewModel
}
var body: some View {
ZStack {
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: [
GridItem(.flexible(minimum: 100, maximum: 200), alignment: .top),
GridItem(.flexible(minimum: 100, maximum: 200), alignment: .top)
], spacing: 12, content: {
ForEach(0 ..< viewModel.movies.count, id: \.self) { index in
viewModel.viewForMovie(viewModel.movies[index])
}
})
.id(UUID()) //For ignoring animation
}
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: Color.blue))
.scaleEffect(1.5, anchor: .center)
.opacity(self.viewModel.loadingMovies ? 1 : 0)
}.onAppear { self.viewModel.loadMovies() }
}
}
struct MovieView: View {
#ObservedObject var viewModel: MovieViewModel
var onDelete: (Movie) -> Void
init(viewModel: MovieViewModel, onDelete: #escaping (Movie) -> Void) {
self.viewModel = viewModel
self.onDelete = onDelete
}
var body: some View {
TmdbMovieView(vm: self.viewModel.tmdbViewModel)
.contentShape(RoundedRectangle(cornerRadius: Constants.MovieViews.movieViewCornerRadius, style: .continuous)) //Prevents sharp edges on context menu
.contextMenu {
Button {
self.onDelete(viewModel.movie)
} label: {
Label("Remove from list", systemImage: Constants.Icons.remove)
}
}
}
}
Here's the movie list view model which specifies what view to use for the movies and exposes the movies from the movie list controller
class MovieListViewModel: ObservableObject {
let name: String
var movies: [Movie] {
return self.movieListController.movies
}
#Published var loadingMovies = false
let movieListController: MovieListController
var viewForMovie: (Movie) -> MovieView
init(name: String, movieListController: MovieListController, viewForMovie: #escaping (Movie) -> MovieView) {
self.name = name
self.movieListController = movieListController
self.viewForMovie = viewForMovie
}
func loadMovies() {
DispatchQueue.main.async {
self.loadingMovies = true
}
self.movieListController.getMovies(complete: onLoadMoviesComplete)
}
private func onLoadMoviesComplete(success: Bool) {
if !success {
//TODO: Handle load fail
}
DispatchQueue.main.async {
self.loadingMovies = false
}
}
func deleteMovie(movie: Movie) {
self.movieListController.delete(movie: movie, complete: onDeleteComplete)
}
private func onDeleteComplete(success: Bool) {
if success {
loadMovies() //TODO: Need this because movies already triggered a change, but the view won't update???
} else {
//TODO: Handle delete fail
}
}
}
Here's my view model factory that creates the view model instance and ties the delete callback on the movie view to the view model
class ViewModelFactory {
let movieListController = MovieListController(id: 1)
func makeMovieListViewModel() -> MovieListViewModel {
let viewModel = MovieListViewModel(
name: "Test",
movieListController: self.movieListController,
viewForMovie: { [unowned self] in
MovieView(viewModel: self.makeMovieViewModel(for: $0), onDelete: {_ in})
}
)
viewModel.viewForMovie = { [unowned self] in
MovieView(viewModel: self.makeMovieViewModel(for: $0), onDelete: viewModel.deleteMovie)
}
return viewModel
}
func makeMovieViewModel(for movie: Movie) -> MovieViewModel {
return MovieViewModel(movie: movie)
}
}
And here is the controller which handles actually hitting my api. The get movies function updates the movies array and the UI updates accordingly. However, the delete function does not cause the UI to update
class MovieListController: ObservableObject {
#Published var movies: [Movie] = []
private var id: Int
init(id: Int) {
self.id = id
}
func getMovies(complete: #escaping (Bool) -> Void) {
guard let moviesUrl = URL(string: "\(Constants.Urls.movieLists)/\(id)\(Constants.Urls.moviesPath)") else {
print("Invalid url...")
complete(false)
return
}
URLSession.shared.dataTask(with: moviesUrl) { data, response, error in
guard let data = data else { return }
do {
var decodedMovies = try JSONDecoder().decode([Movie].self, from: data)
decodedMovies.sort { $0.id < $1.id }
self.movies = decodedMovies
complete(true)
} catch {
print("Failed to decode: \(error)")
complete(false)
}
}.resume()
}
func delete(movie: Movie, complete: #escaping (Bool) -> Void) {
guard let deleteMovieUrl = URL(string: "\(Constants.Urls.movieLists)/\(self.id)\(Constants.Urls.moviesPath)/\(movie.id)") else {
print("Invalid url...")
complete(false)
return
}
var request = URLRequest(url: deleteMovieUrl)
request.httpMethod = "DELETE"
URLSession.shared.dataTask(with: request) { data, response, error in
guard let response = response as? HTTPURLResponse,
error == nil else { // check for fundamental networking error
print("error", error ?? "Unknown error")
complete(false)
return
}
guard (200 ... 299) ~= response.statusCode else { // check for http errors
print("statusCode should be 2xx, but is \(response.statusCode)")
print("response = \(response)")
complete(false)
return
}
if let idx = self.movies.firstIndex(where: { $0.id == movie.id }) {
self.movies.remove(at: idx)
}
complete(true)
}.resume()
}
}

Update view with observed objects of observed array in swiftui

I'm having an Image holder that would load the thumdnail on init and allow for download later on. My issue is that the view is not updated with the images after I load them. After pressing the load button for the second time, my first images are then displayed.
I'm having trouble finding the reason behind this behaviour.
The image holder :
class MyImage: ObservableObject {
private static let sessionProcessingQueue = DispatchQueue(label: "SessionProcessingQueue")
#Published var thumbnail: UIImage?
#Published var loaded: Bool
var fullName: String {
"\(folderName)/\(fileName)"
}
var onThumbnailSet: ((UIImage?) -> Void)
private var folderName: String
private var fileName: String
private var cancelableThumbnail: AnyCancellable?
private var thumbnailUrl: URL? {
return URL(string: "\(BASE_URL)/thumbnail/\(fullName)")
}
private var downloadUrl: URL? {
return URL(string: "\(BASE_URL)/download/\(fullName)")
}
init(folderName: String, fileName: String) {
self.folderName = folderName
self.fileName = fileName
self.loaded = false
self.loadThumbnail()
}
private func loadThumbnail() {
guard let requestUrl = thumbnailUrl else { fatalError() }
self.cancelableThumbnail = URLSession.shared.dataTaskPublisher(for: requestUrl)
.subscribe(on: Self.sessionProcessingQueue)
.map { UIImage(data: $0.data) }
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { (suscriberCompletion) in
switch suscriberCompletion {
case .finished:
break
case .failure(let error):
print(error.localizedDescription)
}
}, receiveValue: { [weak self] (value) in
self?.objectWillChange.send()
self?.loaded.toggle()
self?.thumbnail = value
})
}
The view :
struct MyView: View {
#ObservedObject var imagesHolder: ImagesHolder = ImagesHolder()
var body: some View {
VStack {
Button(action: {
self.loadImages()
}, label: {
Text("Load images")
})
ForEach(imagesHolder.images, id: \.self) { image in
if image.loaded {
Image(uiImage: image.thumbnail!)
.frame(width: 600, height: 600)
} else {
Text("Not loaded")
}
}
}
}
private func loadImages() -> Void {
loadMediaList(
onLoadDone: { myImages in
myImages.forEach { image in
imagesHolder.append(image)
}
}
)
}
}
The observed object containing the array of loaded images :
class ImagesHolder: ObservableObject {
#Published var images: [MyImage] = []
func append(_ myImage: MyImage) {
objectWillChange.send()
images.append(myImage)
}
}
And finally my data loader :
func loadMediaList(onLoadDone: #escaping (([MyImage]) -> Void)) -> AnyCancellable {
let url = URL(string: "\(BASE_URL)/medias")
guard let requestUrl = url else { fatalError() }
return URLSession.shared.dataTaskPublisher(for: requestUrl)
.subscribe(on: Self.sessionProcessingQueue)
.map { parseJSON(data: $0.data) }
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { (suscriberCompletion) in
switch suscriberCompletion {
case .finished:
break
case .failure(let error):
print(error.localizedDescription)
}
}, receiveValue: { images in
onLoadDone(images);
})
}
What I ended up doing and worked great for me was having a seperate view for the display of my Image like this :
struct MyImageView: View {
#ObservedObject var image: MyImage
init(image: MyImage) {
self.image = image
}
var body: some View {
if image.loaded {
Image(uiImage: image.thumbnail!)
.resizable()
} else {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.frame(width: 100, height: 100, alignment: .center)
}
}
}
struct MyView: View {
#ObservedObject var imagesHolder: ImagesHolder = ImagesHolder()
var body: some View {
VStack {
Button(action: {
self.loadImages()
}, label: {
Text("Load images")
})
ForEach(imagesHolder.images, id: \.self) { image in
MyImageView(image: image)
}
}
}
private func loadImages() -> Void {
loadMediaList(
onLoadDone: { myImages in
myImages.forEach { image in
imagesHolder.append(image)
}
}
)
}
}

Downloading images from S3 and showing them using SwiftUI results in high memory usage

Hey 👋 So I am downloading images from AWS S3 and show them in my app using a swiftUI LazyVGrid.
My code to download is the following:
class S3CacheFetcher: Fetcher {
typealias KeyType = MediaItemCacheInfo
typealias OutputType = NSData
func get(_ key: KeyType) -> AnyPublisher<OutputType, Error> {
return download(mediaItem: key).eraseToAnyPublisher()
}
private func download(mediaItem: KeyType) -> AnyPublisher<OutputType, Error>{
let BUCKET = "someBucket"
return Deferred {
Future { promise in
guard let key:String = S3CacheFetcher.getItemKey(mediaItem: mediaItem) else { fatalError("UserPoolID Error") }
print("Downloading image with key: \(key)")
AWSS3TransferUtility.default().downloadData(fromBucket: BUCKET,
key: key,
expression: nil) { (task, url, data, error) in
if let error = error{
print(error)
promise(.failure(error))
}else if let data = data{
// EDIT--------
let encrypt = S3CacheFetcher.encrypt(data: data)
let decrypt = S3CacheFetcher.decrypt(data: encrypt)
// EDIT--------
promise(.success(decrypt as NSData))
}
}
}
}
.eraseToAnyPublisher()
}
....
// EDIT---------
// In my code I have a static function that decrypts the images using CryptoKit.AES.GCM
// To test my problem I added these two functions that should stand for my decryption.
static var symmetricKey = SymmetricKey(size: .bits256)
static func encrypt(data: Data) -> Data{
return try! AES.GCM.seal(data, using: S3CacheFetcher.symmetricKey).combined!
}
static func decrypt(data: Data) -> Data{
return try! AES.GCM.open(AES.GCM.SealedBox(combined: data), using: S3CacheFetcher.symmetricKey)
}
}
My GridView:
struct AllPhotos: View {
#StateObject var mediaManager = MediaManager()
var body: some View {
ScrollView{
LazyVGrid(columns: columns, spacing: 3){
ForEach(mediaManager.mediaItems) { item in
VStack{
ImageView(downloader: ImageLoader(mediaItem: item, size: .large, parentAlbum: nil))
}
}
}
}
}
My ImageView I am using inside my GridView:
struct ImageView: View{
#StateObject var downloader: ImageLoader
var body: some View {
Image(uiImage: downloader.image ?? UIImage(systemName: "photo")!)
.resizable()
.aspectRatio(contentMode: .fill)
.onAppear(perform: {
downloader.load()
})
.onDisappear {
downloader.cancel()
}
}
}
And last but not least the ImageDownloader which is triggered when the image view is shown:
class ImageLoader: ObservableObject {
#Published var image: UIImage?
private(set) var isLoading = false
private var cancellable: AnyCancellable?
private(set) var mediaItem:MediaItem
private(set) var size: ThumbnailSizes
private(set) var parentAlbum: GetAlbum?
init(mediaItem: MediaItem, size: ThumbnailSizes, parentAlbum: GetAlbum?) {
self.mediaItem = mediaItem
self.size = size
self.parentAlbum = parentAlbum
}
deinit {
cancel()
self.image = nil
}
func load() {
guard !isLoading else { return }
// I use the Carlos cache library but for the sake of debugging I just use my Fetcher like below
cancellable = S3CacheFetcher().get(.init(parentAlbum: self.parentAlbum, size: self.size, cipher: self.mediaItem.cipher, ivNonce: self.mediaItem.ivNonce, mid: self.mediaItem.mid))
.map{ UIImage(data: $0 as Data)}
.replaceError(with: nil)
.handleEvents(receiveSubscription: { [weak self] _ in self?.onStart() },
receiveCompletion: { [weak self] _ in self?.onFinish() },
receiveCancel: { [weak self] in self?.onFinish() })
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.image = $0 }
}
func cancel() {
cancellable?.cancel()
self.image = nil
}
private func onStart() {
isLoading = true
}
private func onFinish() {
isLoading = false
}
}
So first of all before I describe my problem. Yes I know I have to cache those images for a smother experience. I did that but for the sake of debugging my memory issue I do not cache those images for now.
Expected behavior: Downloads images and displays them if the view is shown. Purges the images out of memory if the image view is not shown.
Actual behavior: Downloads the images and displays them but it does not purge them from memory once the image view has disappeared. If I scroll up and down for some period of time the memory usage is up in the Gb range and the app crashes.
If I use my persistent cache which grabs the images from disk with more or less the same logic for grabbing and displaying the images than everything works as expected and the memory usage is not higher than 50 Mb.
I am fairly new to Combine as well as SwiftUI so any help is much appreciated.

How to fetch Link metadata in SwiftUI using LinkPresentation?

According to WWDC20 and other articles it seems its quite easy to fetch the image url from a given url. Below is my starter code. That just lists a random list of urls and is supposed to fetch the imageurl for the rich link previews of the urls.
One simply fetches the metadata using the LPMetadataProvider. But i can't get it to show the image url. Does someone know how its done in SwiftUI?
import SwiftUI
import LinkPresentation
struct ExampleHTTPLinks: View {
var links = [ "https://www.google.com", "https://www.hotmail.com"]
let metadataProvider = LPMetadataProvider()
var body: some View {
List(links, id:\.self) { item in
HStack {
Text(item)
Image(systemName: "heart.fill")
metadataProvider.startFetchingMetadata(for: URL(string: item)!) { metadata, error in
if error != nil {
// The fetch failed; handle the error.
// Examples: server doesn't respond, is too slow, user doesn't have network.
return
}
let linkView = LPLinkView(metadata: metadata)
Image(linkView.image)
// Make use of the fetched metadata.
}
}
}
}
}
struct ExampleHTTPLinks_Previews: PreviewProvider {
static var previews: some View {
ExampleHTTPLinks()
}
}
Here's a working version:
class LinkViewModel : ObservableObject {
let metadataProvider = LPMetadataProvider()
#Published var metadata: LPLinkMetadata?
#Published var image: UIImage?
init(link : String) {
guard let url = URL(string: link) else {
return
}
metadataProvider.startFetchingMetadata(for: url) { (metadata, error) in
guard error == nil else {
assertionFailure("Error")
return
}
DispatchQueue.main.async {
self.metadata = metadata
}
guard let imageProvider = metadata?.imageProvider else { return }
imageProvider.loadObject(ofClass: UIImage.self) { (image, error) in
guard error == nil else {
// handle error
return
}
if let image = image as? UIImage {
// do something with image
DispatchQueue.main.async {
self.image = image
}
} else {
print("no image available")
}
}
}
}
}
struct MetadataView : View {
#StateObject var vm : LinkViewModel
var body: some View {
VStack {
if let metadata = vm.metadata {
Text(metadata.title ?? "")
}
if let uiImage = vm.image {
Image(uiImage: uiImage)
.resizable()
.frame(width: 100, height: 100)
}
}
}
}
struct ContentView: View {
var links = [ "https://www.google.com", "https://www.hotmail.com"]
let metadataProvider = LPMetadataProvider()
var body: some View {
List(links, id:\.self) { item in
HStack {
Text(item)
Image(systemName: "heart.fill")
MetadataView(vm: LinkViewModel(link: item))
}
}
}
}
LPMetadataProvider complains if you try to use it for multiple calls, so I've moved it to a view model.
The image is vended by an NSImageProvider -- you can see the loadObject call is what gets the UIImage out of it.
Note that you could use LPLinkView if you wanted to use the out-of-the-box presentation that Apple gives you. Because it's a UIView, to use it in SwiftUI, you'd have to wrap it with UIViewRepresentable:
struct LPLinkViewRepresented: UIViewRepresentable {
var metadata: LPLinkMetadata
func makeUIView(context: Context) -> LPLinkView {
return LPLinkView(metadata: metadata)
}
func updateUIView(_ uiView: LPLinkView, context: Context) {
}
}
struct MetadataView : View {
#StateObject var vm : LinkViewModel
var body: some View {
if let metadata = vm.metadata {
LPLinkViewRepresented(metadata: metadata)
} else {
EmptyView()
}
}
}
class LinkViewModel : ObservableObject {
let metadataProvider = LPMetadataProvider()
#Published var metadata: LPLinkMetadata?
init(link : String) {
guard let url = URL(string: link) else {
return
}
metadataProvider.startFetchingMetadata(for: url) { (metadata, error) in
guard error == nil else {
assertionFailure("Error")
return
}
DispatchQueue.main.async {
self.metadata = metadata
}
}
}
}

How to reload data in SwiftUI List?

I'm new to iOS dev, so sorry if it's an obvious question. But I can't figure out how to update the data in SwiftUI List. I'm fetching the data from API and using #ObservedObject to pass it to the ContentView. It works fine when I'm launching my app, but after I change my API request (by typing a keyword in the SearchBar) and fetch it again, it doesn't seem to update the List, even though the data was changed.
ContentView.swift
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
#State var searchText: String = ""
var body: some View {
NavigationView{
VStack {
SearchBar(text: $searchText, placeholder: "Enter a keyword")
List(networkManager.posts) { post in
NavigationLink(destination: DetailView(url: post.url)) {
HStack {
Text(post.title)
}
}
}.gesture(DragGesture().onChanged { _ in UIApplication.shared.endEditing() })
}.navigationBarTitle("News")
}
.onAppear {
self.networkManager.fetchData(self.searchText)
}
}
}
NetworkManager.swift
class NetworkManager: ObservableObject {
#Published var posts = [Post]()
func fetchData(_ keyword: String?){
var urlString = "https://newsapi.org/v2/top-headlines?country=us&apiKey=5dcef32f4c69413e8fe128cc5c7ba4cf"
if keyword != nil {
urlString = "https://newsapi.org/v2/top-headlines?country=us&apiKey=5dcef32f4c69413e8fe128cc5c7ba4cf&q=\(keyword!)"
}
print(urlString)
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil{
let decoder = JSONDecoder()
if let safeData = data{
do{
let results = try decoder.decode(News.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.articles
print(self.posts)
}
} catch{
print(error)
}
}
}
}
task.resume()
}
}
}
SearchBar.swift (I fetch data again inside searchBarSearchButtonClicked)
struct SearchBar: UIViewRepresentable {
#Binding var text: String
var placeholder: String
class Coordinator: NSObject, UISearchBarDelegate {
#ObservedObject var networkManager = NetworkManager()
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
print(text)
DispatchQueue.main.async {
self.networkManager.fetchData(self.text)
}
UIApplication.shared.endEditing()
}
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.placeholder = placeholder
searchBar.searchBarStyle = .minimal
searchBar.autocapitalizationType = .none
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
News.swift
struct News: Decodable {
let articles: [Post]
}
struct Post: Decodable, Identifiable {
var id: String{
return url!
}
let title: String
let url: String?
}
I've made a few minor modifications and made the code work in Xcode-playgrounds. Here's how:
Model:
struct News: Codable { var articles: [Post] }
struct Post: Identifiable, Codable { var title: String; var id: String { title } }
ContentView:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView {
VStack {
TextField("Enter a keyword", text: $networkManager.searchText)
List(networkManager.posts) { post in
NavigationLink(destination: EmptyView()) {
HStack {
Text(post.title)
}
}
}
}.navigationBarTitle("News")
}
.onAppear {
self.networkManager.fetchData()
}
}
}
NetworkManager:
class NetworkManager: ObservableObject {
#Published var searchText: String = "" {
didSet {
fetchData()
}
}
#Published var posts = [Post]()
func fetchData() {
let urlString = "https://newsapi.org/v2/top-headlines?country=us&apiKey=5dcef32f4c69413e8fe128cc5c7ba4cf&q=\(searchText)"
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil{
let decoder = JSONDecoder()
if let safeData = data{
do{
let results = try decoder.decode(News.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.articles
print(self.posts)
}
} catch{
print(error)
}
}
}
}
task.resume()
}
}
}