I have a View
#State private var showPopover = false
#State private var selectedFile: File?
func openFilePicker() {
self.openFilePicker()
}
var body: some View {
it has a Button
Button(action: {
self.showPopover.toggle()
}) {
it has thses handlers:
}.sheet(isPresented: $showPopover) {
FileBrowser(selectedFile: $selectedFile)
}
.sheet(item: $selectedFile) { file in
Text("File selected: \(file.url.lastPathComponent)")
}
and these extra structs:
struct File: Identifiable {
let id = UUID()
let url: URL
}
struct FileBrowser: View {
#Binding var selectedFile: File?
#State private var files = [File]()
var body: some View {
List(files, id: \.id) { file in
Button(action: {
self.selectedFile = file
}) {
Text(file.url.lastPathComponent)
}
}
.onAppear {
let fm = FileManager.default
let documentsURL = try! fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
self.files = try! fm.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil, options: []).map { File(url: $0) }
}
}
}
But nothing appears in the popup. Empty.
Why this line return nothing?
self.files = try! fm.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil, options: []).map { File(url: $0) }
Related
Here I get the image from the link
struct CustomImageView: View {
var urlString: String
#ObservedObject var imageLoader = ImageLoaderService()
#State var image: UIImage = UIImage()
var body: some View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:100, height:100)
.onReceive(imageLoader.$image) { image in
self.image = image
}
.onAppear {
imageLoader.loadImage(for: urlString)
}
}
}
class ImageLoaderService: ObservableObject {
#Published var image: UIImage = UIImage()
func loadImage(for urlString: String) {
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.image = UIImage(data: data) ?? UIImage()
}
}
task.resume()
}
}
Here I display the received photo by the link and try to save it via .contextMenu
VStack {
CustomImageView(urlString: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png")
.contextMenu {
Button(action: {
}) {
HStack {
Text("Save image")
Image(systemName: "square.and.arrow.down.fill")
}
}
}
}
When you click on a photo, a .contextMenu opens with a save button, but the photo is not saved, what should I do?
in your button action add
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
where image is a UIImage, so you need to pass the image from your model and pass it to the above snippet.
Full Code:
struct CustomImageView: View {
var urlString: String
#ObservedObject var imageLoader: ImageLoaderService
#State var image: UIImage = UIImage()
var body: some View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:100, height:100)
.onReceive(imageLoader.$image) { image in
self.image = image
}.onAppear {
imageLoader.loadImage(for: urlString)
}
}
}
class ImageLoaderService: ObservableObject {
#Published var image: UIImage = UIImage()
func loadImage(for urlString: String) {
guard let url = URL(string: urlString) else {return}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else {return}
DispatchQueue.main.async {
self.image = UIImage(data: data) ?? UIImage()
}
}
task.resume()
}
}
struct ContentView: View {
#ObservedObject var imageLoader = ImageLoaderService()
var body: some View {
VStack {
CustomImageView(urlString: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png", imageLoader: imageLoader)
.contextMenu {
Button(action: {
UIImageWriteToSavedPhotosAlbum(imageLoader.image, nil, nil, nil)
}) {
HStack {
Text("Save image")
Image(systemName: "square.and.arrow.down.fill")
}
}
}
}
}
}
I have a code that uses ReplayKit to record the screen, and saves it in the documents Directory of the app. The problem comes when I am trying to access it, it does not find the URL of the video.
import SwiftUI
import ReplayKit
import AVKit
struct ContentView: View {
#State var screenRecording = ScreenRecording.singleton
#State var buttonMessage: String = "Start recording"
#State var showVideo: Bool = false
var body: some View {
Button(buttonMessage) {
if screenRecording.recording {
screenRecording.stopRecording()
buttonMessage = "Start recording"
} else {
screenRecording.startRecording()
buttonMessage = "Stop recording"
}
screenRecording.recording = !screenRecording.recording
}
Button("next screen") {
showVideo = true
}
if showVideo {
VideoPlayer(player: AVPlayer(url: screenRecording.getDocument()))
.frame(height: 400)
.onAppear {
print("URL: \(screenRecording.getDocument().path)")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class ScreenRecording: ObservableObject {
var recording: Bool = false
private let screenRecorder = RPScreenRecorder.shared()
static let singleton = ScreenRecording()
private init() {}
func startRecording() {
if !screenRecorder.isRecording && RPScreenRecorder.shared().isAvailable {
screenRecorder.isMicrophoneEnabled = true
screenRecorder.isCameraEnabled = true
screenRecorder.startRecording { err in
guard err == nil else {
print("Failed to start recording...")
return
}
print("Recording starts!")
}
} else {
print("Trying to start recording when a recording is already happening!")
}
}
func stopRecording() {
if screenRecorder.isRecording {
screenRecorder.stopRecording(withOutput: getDocumentDirectoryURL()) { err in
guard err == nil else {
print("An error has ocurred: \(String(describing: err))")
return
}
}
print("Recording stops!")
} else {
print("Trying to stop recording when NO recording is initiated!")
}
}
func getDocumentDirectoryURL() -> URL {
let documentDirURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
print("Filepath is: \(documentDirURL.path)")
return documentDirURL
}
func getDocument() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
print("This re the paths: \(paths)")
return paths[0]
}
}
The method getDocument should return the first URL of the directory, which should be the video file, but it does not work. Where is the problem?
You can check Apple's demo
Recording and Streaming Your macOS App
The relevant code is
func exportClip() {
let clipURL = getDirectory()
let interval = TimeInterval(5)
print("Generating clip at URL: ", clipURL)
RPScreenRecorder.shared().exportClip(to: clipURL, duration: interval) { error in
if error != nil {
print("Error attempting to start Clip Buffering")
} else {
// There isn't an error, so save the clip at the URL to Photos.
self.saveToPhotos(tempURL: clipURL)
}
}
}
func getDirectory() -> URL {
var tempPath = URL(fileURLWithPath: NSTemporaryDirectory())
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd-hh-mm-ss"
let stringDate = formatter.string(from: Date())
print(stringDate)
tempPath.appendPathComponent(String.localizedStringWithFormat("output-%#.mp4", stringDate))
return tempPath
}
now use stopRecording(withOutput: , instead of exportClip(to:
And more:
according to apple's doc
stopRecording(withOutput:completionHandler:)
the movie is written to the specified output URL.
we need the mp4 file
When fetching data from my database, I update an array from the json received inside the fetching function. While inside the function, the array stays updated, but when I try to access the array in a separate view, the array is empty.
the code that fetches the data and updates the "events" array looks like this:
import Foundation
import SwiftUI
let apiUrl = "http://localhost:5000/"
class DataFetcher: ObservableObject {
#Published var events: [eventdata] = []
func fetchEvents(){
events.removeAll()
let url = NSMutableURLRequest(url: NSURL(string: apiUrl)! as URL)
url.httpMethod = "GET"
URLSession.shared.dataTask(with: url as URLRequest) { data, response, err in
if let err = err {
print(err.localizedDescription)
return
}
guard let response = response as? HTTPURLResponse else { return }
if response.statusCode == 200 {
guard let data = data else { return }
DispatchQueue.main.async {
do{
self.events = try JSONDecoder().decode([eventdata].self, from: data)
// print(self.events.last?.address)
}catch let err {
print("Error: \(err)")
}
}
} else {
print("HTTPURLResponse code: \(response.statusCode)")
}
}.resume()
//print(self.events.last?.address)
}
}
The view that calls this function looks like this:
import Foundation
import SwiftUI
struct CreateEventButton: View {
#ObservedObject var request = DataFetcher()
#State private var isPresentedEvent = false
#State private var eventName: String = ""
#State private var eventDescription: String = ""
#State private var selectedStartTime = Date()
#State private var selectedEndTime = Date()
#State private var startTimeStamp: String = ""
#State private var endTimeStamp: String = ""
#Binding var annotationSelected: Bool
var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .full
return formatter
}
func send(_ sender: Any) {
let request = NSMutableURLRequest(url: NSURL(string: "http://localhost:5000/")! as URL)
request.httpMethod = "POST"
self.startTimeStamp = "\(self.selectedStartTime)"
self.endTimeStamp = "\(self.selectedEndTime)"
self.startTimeStamp.removeLast(5)
self.endTimeStamp.removeLast(5)
let postString = "b=\(self.eventName)&c=\(self.eventDescription)&d=\(self.startTimeStamp)&e=\(self.endTimeStamp)"
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request as URLRequest) {
data, response, error in
if error != nil {
print("error=\(String(describing: error))")
return
}
print("response = \(String(describing: response))")
let responseString = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
print("responseString = \(String(describing: responseString))")
}
task.resume()
self.eventName = ""
self.eventDescription = ""
self.selectedStartTime = Date()
self.selectedEndTime = Date()
}
var body: some View {
//UNCOMMENT THIS STUFF BELOW TO MAKE AN EVENT CREATION BUTTON IN THE SHEET INSTEAD OF JUST DIRECTING TO A SHEET WITH THE EVENT CREATION DETAILS
Button(action: {
self.isPresentedEvent.toggle() //trigger modal presentation
}, label: {
Text("Create Event").font(.system(size: 18)).foregroundColor(Color(.darkGray)).shadow(radius: 8)
}).padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.secondary)
.background(Color(.secondarySystemBackground))
.cornerRadius(50.0)
.sheet(isPresented: $isPresentedEvent, content:{
VStack{
TextField("Event Name", text: self.$eventName).padding()
TextField("Event Description", text: self.$eventDescription).padding()
Form {
DatePicker("When your event starts: ", selection: self.$selectedStartTime, in: Date()...)
}
Form {
DatePicker("When your event ends: ", selection: self.$selectedEndTime, in: Date()...)
}
HStack{
Button(action: {
self.isPresentedEvent.toggle()
self.annotationSelected = false
print("Start: \(self.selectedStartTime)")
print("End: \(self.selectedEndTime)")
self.send((Any).self)
}, label: {
Text("Create Event")
})
Button(action: {
self.isPresentedEvent.toggle()
self.request.fetchEvents()
print("yo yo \(self.request.events.last?.address)")
}, label: {
Text("Cancel")
})
}
Text("Create Event Button (Non Functional)").padding()
}
} )
}
}
Any insight on where I might be going wrong is much appreciated
Button(action: {
self.isPresentedEvent.toggle()
self.request.fetchEvents()
}, label: {
if self.request.events.count > 0 {
Text("hey we have requests!")
} else {
Text("Cancel")
}
})
woops posted the accidently before finishing. one sec.
I believe it's a timing issue. The view is rendered before your request finishes fetching your data. You can add an if statement like above to update your view once the data is fully fetched.
I want to display an image from a url retrieved in json in my list. How would I Do so?
I tried just calling image and entering the url, but it just shows the space for the image, but not the actual image.
var body: some View {
NavigationView {
List {
TextField("Search for Meme by name", text: self.$searchItem)
ForEach(viewModel.memes) { meme in
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(meme.name).font(.headline).lineLimit(nil)
Image(meme.url).resizable().frame(width: 100, height: 100)
}
}
}
}
.navigationBarTitle("All Memes")
}.onAppear {
self.viewModel.fetchAllMemes()
}
}
Make your own view that has its own ObservableObject that downloads (and optionally caches) the image. Here is an example:
import SwiftUI
import Combine
import UIKit
class ImageCache {
enum Error: Swift.Error {
case dataConversionFailed
case sessionError(Swift.Error)
}
static let shared = ImageCache()
private let cache = NSCache<NSURL, UIImage>()
private init() { }
static func image(for url: URL) -> AnyPublisher<UIImage?, ImageCache.Error> {
guard let image = shared.cache.object(forKey: url as NSURL) else {
return URLSession
.shared
.dataTaskPublisher(for: url)
.tryMap { (tuple) -> UIImage in
let (data, _) = tuple
guard let image = UIImage(data: data) else {
throw Error.dataConversionFailed
}
shared.cache.setObject(image, forKey: url as NSURL)
return image
}
.mapError({ error in Error.sessionError(error) })
.eraseToAnyPublisher()
}
return Just(image)
.mapError({ _ in fatalError() })
.eraseToAnyPublisher()
}
}
class ImageModel: ObservableObject {
#Published var image: UIImage? = nil
var cacheSubscription: AnyCancellable?
init(url: URL) {
cacheSubscription = ImageCache
.image(for: url)
.replaceError(with: nil)
.receive(on: RunLoop.main, options: .none)
.assign(to: \.image, on: self)
}
}
struct RemoteImage : View {
#ObservedObject var imageModel: ImageModel
init(url: URL) {
imageModel = ImageModel(url: url)
}
var body: some View {
imageModel
.image
.map { Image(uiImage:$0).resizable() }
?? Image(systemName: "questionmark").resizable()
}
}
In SwiftUI there are some .init methods to create an Image but none of them admits a block or any other way to load an UIImage from network/cache...
I am using Kingfisher to load images from network and cache inside a list row, but the way to draw the image in the view is to re-render it again, which I would prefer to not do. Also, I am creating a fake image(only coloured) as placeholder while the image gets fetched.
Another way would be to wrap all inside a custom view and only re-render the wrapper. But I haven't tried yet.
This sample is working right now.
Any idea to improve the current one will be great
Some view using the loader
struct SampleView : View {
#ObjectBinding let imageLoader: ImageLoader
init(imageLoader: ImageLoader) {
self.imageLoader = imageLoader
}
var body: some View {
Image(uiImage: imageLoader.image(for: "https://url-for-image"))
.frame(width: 128, height: 128)
.aspectRatio(contentMode: ContentMode.fit)
}
}
import UIKit.UIImage
import SwiftUI
import Combine
import class Kingfisher.ImageDownloader
import struct Kingfisher.DownloadTask
import class Kingfisher.ImageCache
import class Kingfisher.KingfisherManager
class ImageLoader: BindableObject {
var didChange = PassthroughSubject<ImageLoader, Never>()
private let downloader: ImageDownloader
private let cache: ImageCache
private var image: UIImage? {
didSet {
dispatchqueue.async { [weak self] in
guard let self = self else { return }
self.didChange.send(self)
}
}
}
private var task: DownloadTask?
private let dispatchqueue: DispatchQueue
init(downloader: ImageDownloader = KingfisherManager.shared.downloader,
cache: ImageCache = KingfisherManager.shared.cache,
dispatchqueue: DispatchQueue = DispatchQueue.main) {
self.downloader = downloader
self.cache = cache
self.dispatchqueue = dispatchqueue
}
deinit {
task?.cancel()
}
func image(for url: URL?) -> UIImage {
guard let targetUrl = url else {
return UIImage.from(color: .gray)
}
guard let image = image else {
load(url: targetUrl)
return UIImage.from(color: .gray)
}
return image
}
private func load(url: URL) {
let key = url.absoluteString
if cache.isCached(forKey: key) {
cache.retrieveImage(forKey: key) { [weak self] (result) in
guard let self = self else { return }
switch result {
case .success(let value):
self.image = value.image
case .failure(let error):
print(error.localizedDescription)
}
}
} else {
downloader.downloadImage(with: url, options: nil, progressBlock: nil) { [weak self] (result) in
guard let self = self else { return }
switch result {
case .success(let value):
self.cache.storeToDisk(value.originalData, forKey: url.absoluteString)
self.image = value.image
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
}
SwiftUI 3
Starting from iOS 15 we can now use AsyncImage:
AsyncImage(url: URL(string: "https://example.com/icon.png")) { image in
image.resizable()
} placeholder: {
ProgressView()
}
.frame(width: 50, height: 50)
SwiftUI 2
Here is a native SwiftUI solution that supports caching and multiple loading states:
import Combine
import SwiftUI
struct NetworkImage: View {
#StateObject private var viewModel = ViewModel()
let url: URL?
var body: some View {
Group {
if let data = viewModel.imageData, let uiImage = UIImage(data: data) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
} else if viewModel.isLoading {
ProgressView()
} else {
Image(systemName: "photo")
}
}
.onAppear {
viewModel.loadImage(from: url)
}
}
}
extension NetworkImage {
class ViewModel: ObservableObject {
#Published var imageData: Data?
#Published var isLoading = false
private static let cache = NSCache<NSURL, NSData>()
private var cancellables = Set<AnyCancellable>()
func loadImage(from url: URL?) {
isLoading = true
guard let url = url else {
isLoading = false
return
}
if let data = Self.cache.object(forKey: url as NSURL) {
imageData = data as Data
isLoading = false
return
}
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] in
if let data = $0 {
Self.cache.setObject(data as NSData, forKey: url as NSURL)
self?.imageData = data
}
self?.isLoading = false
}
.store(in: &cancellables)
}
}
}
(The above code doesn't use any third-party libraries, so it's easy to change the NetworkImage in any way.)
Demo
import Combine
import SwiftUI
struct ContentView: View {
#State private var showImage = false
var body: some View {
if showImage {
NetworkImage(url: URL(string: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png"))
.frame(maxHeight: 150)
.padding()
} else {
Button("Load") {
showImage = true
}
}
}
}
(I used an exceptionally large Stack Overflow logo to show the loading state.)
Pass your Model to ImageRow struct which contains url.
import SwiftUI
import Combine
struct ContentView : View {
var listData: Post
var body: some View {
List(model.post) { post in
ImageRow(model: post) // Get image
}
}
}
/********************************************************************/
// Download Image
struct ImageRow: View {
let model: Post
var body: some View {
VStack(alignment: .center) {
ImageViewContainer(imageUrl: model.avatar_url)
}
}
}
struct ImageViewContainer: View {
#ObjectBinding var remoteImageURL: RemoteImageURL
init(imageUrl: String) {
remoteImageURL = RemoteImageURL(imageURL: imageUrl)
}
var body: some View {
Image(uiImage: UIImage(data: remoteImageURL.data) ?? UIImage())
.resizable()
.clipShape(Circle())
.overlay(Circle().stroke(Color.black, lineWidth: 3.0))
.frame(width: 70.0, height: 70.0)
}
}
class RemoteImageURL: BindableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
init(imageURL: String) {
guard let url = URL(string: imageURL) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
DispatchQueue.main.async { self.data = data }
}.resume()
}
}
/********************************************************************/
A simpler and cleaner way to load an image in SwiftUI is to use the renowned Kingfisher library.
Add Kingfisher via Swift Package Manager
Select File > Swift Packages > Add Package Dependency. Enter
https://github.com/onevcat/Kingfisher.git
in the "Choose Package
Repository" dialog. In the next page, specify the version resolving
rule as "Up to Next Major" with "5.8.0" as its earliest version.
After
Xcode checking out the source and resolving the version, you can
choose the "KingfisherSwiftUI" library and add it to your app target.
import KingfisherSwiftUI
KFImage(myUrl)
Done! It's that easy
I would just use the onAppear callback
import Foundation
import SwiftUI
import Combine
import UIKit
struct ImagePreviewModel {
var urlString : String
var width : CGFloat = 100.0
var height : CGFloat = 100.0
}
struct ImagePreview: View {
let viewModel: ImagePreviewModel
#State var initialImage = UIImage()
var body: some View {
Image(uiImage: initialImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: self.width, height: self.height)
.onAppear {
guard let url = URL(string: self.viewModel.urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard let image = UIImage(data: data) else { return }
RunLoop.main.perform {
self.initialImage = image
}
}.resume()
}
}
var width: CGFloat { return max(viewModel.width, 100.0) }
var height: CGFloat { return max(viewModel.height, 100.0) }
}
Define the imageLoader as #ObjectBinding:
#ObjectBinding private var imageLoader: ImageLoader
It would make more sense to init the view with the url for the image :
struct SampleView : View {
var imageUrl: URL
private var image: UIImage {
imageLoader.image(for: imageUrl)
}
#ObjectBinding private var imageLoader: ImageLoader
init(url: URL) {
self.imageUrl = url
self.imageLoader = ImageLoader()
}
var body: some View {
Image(uiImage: image)
.frame(width: 200, height: 300)
.aspectRatio(contentMode: ContentMode.fit)
}
}
For example :
//Create a SampleView with an initial photo
var s = SampleView(url: URL(string: "https://placebear.com/200/300")!)
//You could then update the photo by changing the imageUrl
s.imageUrl = URL(string: "https://placebear.com/200/280")!
import SwiftUI
struct UrlImageView: View {
#ObservedObject var urlImageModel: UrlImageModel
init(urlString: String?) {
urlImageModel = UrlImageModel(urlString: urlString)
}
var body: some View {
Image(uiImage: urlImageModel.image ?? UrlImageView.defaultImage!)
.resizable()
.scaledToFill()
}
static var defaultImage = UIImage(systemName: "photo")
}
class UrlImageModel: ObservableObject {
#Published var image: UIImage?
var urlString: String?
init(urlString: String?) {
self.urlString = urlString
loadImage()
}
func loadImage() {
loadImageFromUrl()
}
func loadImageFromUrl() {
guard let urlString = urlString else {
return
}
let url = URL(string: urlString)!
let task = URLSession.shared.dataTask(with: url, completionHandler:
getImageFromResponse(data:response:error:))
task.resume()
}
func getImageFromResponse(data: Data?, response: URLResponse?, error: Error?)
{
guard error == nil else {
print("Error: \(error!)")
return
}
guard let data = data else {
print("No data found")
return
}
DispatchQueue.main.async {
guard let loadedImage = UIImage(data: data) else {
return
}
self.image = loadedImage
}
}
}
And using like this:
UrlImageView(urlString: "https://developer.apple.com/assets/elements/icons/swiftui/swiftui-96x96_2x.png").frame(width:100, height:100)
With the release of iOS 15 and macOS 12 in 2021, SwiftUI provides native AsyncImage view that enables loading images asynchronously. Bear in mind that you'll still have to fall back to a custom implementation for earlier OS versions.
AsyncImage(url: URL(string: "https://example.com/tile.png"))
The API itself also provides various ways to customise the image or provide a placeholder, for example:
AsyncImage(url: URL(string: "https://example.com/tile.png")) { image in
image.resizable(resizingMode: .tile)
} placeholder: {
Color.green
}
More in the Apple Developer Documentation.