Downloading public PDF file from S3 Bucket SwiftUI - ios

I am simply trying to store a static PDF in an S3 bucket which I can grab and present using Apple's built-in PDF Viewer. I am a having problems and a bit confused on a way to store the PDF locally in the proper form. I apologize if this is repeated or too simple, I have searched for hours on a proper solution but have not found anything that works. Thank you. I tried using the URL directly but that also threw an error.
import Amplify
import SwiftUI
import WebKit
struct ContentView: View {
var body: some View {
containedView()
}
func grabPDF(){
Amplify.Storage.downloadData(
key: "TermsOfUse.pdf"
){ result in
switch result{
case .success(let key):
print("File with key: \(key)")
case .failure(let storageError):
print("Failed: ", storageError)
}
}
}
func containedView() -> WebView{
grabPDF()
return WebView(request: openPDF())
}
func openPDF() -> URLRequest{
let path = Bundle.main.path(forResource: "TermsOfUse", ofType: "pdf")
let url = URL(fileURLWithPath: path!)
print(url)
return URLRequest(url: url)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct WebView: UIViewRepresentable{
let request: URLRequest
func makeUIView(context: Context) -> WKWebView{
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context){
uiView.load(request)
}
}

Amplify.Storage.downloadData returns a Data object -- it doesn't actually download the file. But, there's another method downloadFile that will work for this.
Your applications Bundle has a static set of files. Once you save a file, you won't be looking in the Bundle, but rather in the app's documents or temp directory.
Rather than making the Amplify calls in the view body, probably better to assign it to a separate object (here I'm using an ObservableObject called DataLoader) to do the work at then set a flag (downloaded) when it's done.
class DataLoader : ObservableObject {
#Published var downloaded : Bool = false
func makeRequest() {
let downloadToFileName = getTermsOfUseURL()
Amplify.Storage.downloadFile(
key: "TermsOfUse.pdf",
local: downloadToFileName,
progressListener: { progress in
print("Progress: \(progress)")
}, resultListener: { event in
switch event {
case .success:
print("Completed")
DispatchQueue.main.async { self.downloaded = true }
case .failure(let storageError):
print("Failed: \(storageError.errorDescription). \(storageError.recoverySuggestion)")
}
})
}
func getTermsOfUseURL() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0].appendingPathComponent("TermsOfUse.pdf")
}
}
struct ContentView: View {
#ObservedObject var loader = DataLoader()
var body: some View {
VStack {
if loader.downloaded {
WebView(request: URLRequest(url: loader.getTermsOfUseURL()))
}
}.onAppear {
loader.makeRequest()
}
}
}
Note: I have not tested the Amplify code; it is assumed to work as it is taken from https://docs.amplify.aws/lib/storage/download/q/platform/ios

Related

How to represent a JSON file in SwiftUI?

I'm quite a beginner and trying to get the OpenWeather API JSON to show up in my challenge project.
I managed to model it
struct WeatherRespose: Codable {
var weather: [Weather]
var name: String
}
&
import Foundation
struct Weather: Hashable, Codable {
var main: String
var description: String
}
In addition to fetch the data in ContentView. However, when I try to present it:
#State var weatherForcast = Weather() or #State var weatherForcast = WeatherResponse() I get this error: Missing argument for parameter 'from' in call, insert 'from: <#Decoder#>'
The only thing that worked for me is to present the data in an array:
#State var weatherForcast = [Weather]()
Any idea what am I missing? thank you so much! Ran
I made pretty simple example of how you can do this. There are several additional files, so it's easier to understand how it works in details:
Create additional file called NetworkService, it will fetch weather data:
import Foundation
final class NetworkService{
private let url = "https://example.com"
func performWeatherRequest(completion: #escaping (Result<WeatherResponse, Error>) -> Void){
URLSession.shared.dataTask(with: URL(string: url)!) { data, response, error in
guard let data = data, error == nil else {
completion(.failure(WeatherError.failedToDownload))
return
}
let weatherResponse: WeatherResponse = try! JSONDecoder().decode(WeatherResponse.self, from: data)
completion(.success(weatherResponse))
}.resume()
}
public enum WeatherError: Error {
case failedToDownload
}
}
Create simple ViewModel which will retrieve data from our NetworkService and prepare to present it in ContentView
import Foundation
import SwiftUI
extension ContentView {
#MainActor class ContentViewVM: ObservableObject {
private var networkService = NetworkService()
#Published var currentWeatherMain: String?
#Published var currentWeatherDescription: String?
func fetchWeather(){
networkService.performWeatherRequest { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let weatherResponse):
self?.currentWeatherMain = weatherResponse.weather[0].main
self?.currentWeatherDescription = weatherResponse.weather[0].description
case .failure(_):
print("oops, error occurred")
}
}
}
}
}
}
Add our ContentViewVM to our ContentView:
import SwiftUI
struct ContentView: View {
#StateObject var viewModel = ContentViewVM()
var body: some View {
VStack {
Text("The main is: \(viewModel.currentWeatherMain ?? "")")
Text("The description is: \(viewModel.currentWeatherDescription ?? "")")
}
.onAppear{
viewModel.fetchWeather()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Hope it helps.

How to navigate between two API parameters in SwiftUI

I'm beginner of iOS app development, currently doing iOS & Swift Bootcamp on Udemy by Angela Yu. I have this app called H4X0R News, which shows Hacker News all stories that are on the front/home page on the app by using its API. By the end of a module the app works fine when url property from API json is not nil but there are certain cases when url equals nil. These are posts which instead has story_text property. So what I want here to adjust is add story_text to my code and use it to navigate between this and url parameter. Here's the code I've got:
ContentView.swift
import SwiftUI
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView {
List(networkManager.posts) { post in
NavigationLink(destination: DetailView(url: post.url)) {
HStack {
Text(String(post.points))
Text(post.title)
}
}
}
.navigationTitle("H4X0R NEWS")
}
.onAppear {
self.networkManager.fechData()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
WebView.swift
import Foundation
import WebKit
import SwiftUI
struct WebView: UIViewRepresentable {
let urlString: String?
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
if let safeString = urlString {
if let url = URL(string: safeString) {
let request = URLRequest(url: url)
uiView.load(request)
}
}
}
}
DetailView.swift
import SwiftUI
struct DetailView: View {
let url: String?
var body: some View {
WebView(urlString: url)
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(url: "https://www.google.com/")
}
}
NetworkManager.swift
import Foundation
class NetworkManager: ObservableObject {
#Published var posts = [Post]()
func fechData() {
if let url = URL(string: "http://hn.algolia.com/api/v1/search?tags=front_page") {
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(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.hits
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
PostData.swift
import Foundation
struct Results: Decodable {
let hits: [Post]
}
struct Post: Decodable, Identifiable {
var id: String {
return objectID
}
let objectID: String
let points: Int
let title: String
let url: String?
}
So what I'm sure I need to add this story_text to PostData as String? and then make conditional statement in the WebView.updateUIView() function. Then update the code in other files. But like I said, I'm new in the programming world and I seek for help here for the first time since I've started the course.
If I understand you correct, you want to navigate to a simple text view if there is no URL for the Post?
So you can do it like this:
let text: String?
var body: some View {
if let url = url {
WebView(urlString: url)
} else {
Text(text ?? "-")
}
}

Struggling to implement PDFView in SwiftUI

I am trying to display a PDF in a SwiftUI view.
When trying to implement other solutions here, I get the error Missing arguments for parameters 'PDFName', 'DisplayName' in call
Errors image
I assume that the way you use PDFView() has changed with iOS 15; but I can't seem to find any way to use it in SwiftUI or the docs.
Any help would be greatly appreciated.
Many thanks in advance!
Current attempt at implementation resulting in above error:
import SwiftUI
import PDFKit
struct PDFViewer: View {
var url: URL
var body: some View {
PDFKitRepresentedView(url)
}
}
struct PDFKitRepresentedView: UIViewRepresentable {
let url: URL
init(_ url: URL) {
self.url = url
}
func makeUIView(context: UIViewRepresentableContext<PDFKitRepresentedView>) -> PDFKitRepresentedView.UIViewType {
let pdfView = PDFView()
pdfView.document = PDFDocument(url: self.url)
pdfView.autoScales = true
return pdfView
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PDFKitRepresentedView>) {
}
}
struct PDFView_Previews: PreviewProvider {
static var previews: some View {
PDFViewer(url: Bundle.main.url(forResource: "somePDF", withExtension: "pdf"))
}
}

Why is my #AppStorage not working on SwiftUI?

I'm trying to set up the #AppStorage wrapper in my project.
I'm pulling Texts from a JSON API (see DataModel), and am hoping to store the results in UserDefautls. I want the data to be fetched .OnAppear and stored into the #AppStorage. When the user taps "Get Next Text", I want a new poem to be fetched, and to update #AppStorage with the newest Text data, (which would delete the past Poem stored).
Currently, the code below builds but does not display anything in the Text(currentPoemTitle).
Data Model
import Foundation
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
public class FetchPoem: ObservableObject {
// 1.
#Published var poems = [Poem]()
init() {
getPoem()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
// 2.
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
if let poemData = data {
// 3.
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
}
} else {
print("No data")
}
} catch {
print("Error")
}
}.resume()
}
}
TestView
import SwiftUI
struct Test: View {
#ObservedObject var fetch = FetchPoem()
#AppStorage("currentPoemtTitle") var currentPoemTitle = ""
#AppStorage("currentPoemAuthor") var currentPoemAuthor = ""
var body: some View {
VStack{
Text(currentPoemTitle)
Button("Fetch next text") {
fetch.getPoem()
}
}.onAppear{
if let poem = fetch.poems.first {
currentPoemTitle = "\(poem.title)"
currentPoemAuthor = "\(poem.author)"
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
What am I missing? Thanks.
Here are a few code edits to get you going.
I added AppStorageKeys to manage the #AppStorage keys, to avoid errors retyping key strings (ie. "currentPoemtTitle")
Your question asked how to update the #AppStorage with the data, and the simple solution is to add the #AppStorage variables within the FetchPoem class and set them within the FetchPoem class after the data is downloaded. This also avoids the need for the .onAppear function.
The purpose of using #ObservedObject is to be able to keep your View in sync with the data. By adding the extra layer of #AppStorage, you make the #ObservedObject sort of pointless. Within the View, I added a Text() to display the title using the #ObservedObject values directly, instead of relying on #AppStorage. I'm not sure if you want this, but it would remove the need for the #AppStorage variables entirely.
I also added a getPoems2() function using Combine, which is a new framework from Apple to download async data. It makes the code a little easier/more efficient... getPoems() and getPoems2() both work and do the same thing :)
Code:
import Foundation
import SwiftUI
import Combine
struct AppStorageKeys {
static let poemTitle = "current_poem_title"
static let poemAuthor = "current_poem_author"
}
struct Poem: Codable, Hashable {
let title, author: String
let lines: [String]
let linecount: String
}
public class FetchPoem: ObservableObject {
#Published var poems = [Poem]()
#AppStorage(AppStorageKeys.poemTitle) var poemTitle = ""
#AppStorage(AppStorageKeys.poemAuthor) var poemAuthor = ""
init() {
getPoem2()
}
func getPoem() {
let url = URL(string: "https://poetrydb.org/random/1")!
URLSession.shared.dataTask(with: url) {(data, response, error) in
do {
guard let poemData = data else {
print("No data")
return
}
let decodedData = try JSONDecoder().decode([Poem].self, from: poemData)
DispatchQueue.main.async {
self.poems = decodedData
self.updateFirstPoem()
}
} catch {
print("Error")
}
}
.resume()
}
func getPoem2() {
let url = URL(string: "https://poetrydb.org/random/1")!
URLSession.shared.dataTaskPublisher(for: url)
// fetch on background thread
.subscribe(on: DispatchQueue.global(qos: .background))
// recieve response on main thread
.receive(on: DispatchQueue.main)
// ensure there is data
.tryMap { (data, response) in
guard
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
// decode JSON data to [Poem]
.decode(type: [Poem].self, decoder: JSONDecoder())
// Handle results
.sink { (result) in
// will return success or failure
print("poetry fetch completion: \(result)")
} receiveValue: { (value) in
// if success, will return [Poem]
// here you can update your view
self.poems = value
self.updateFirstPoem()
}
// After recieving response, the URLSession is no longer needed & we can cancel the publisher
.cancel()
}
func updateFirstPoem() {
if let firstPoem = self.poems.first {
self.poemTitle = firstPoem.title
self.poemAuthor = firstPoem.author
}
}
}
struct Test: View {
#ObservedObject var fetch = FetchPoem()
#AppStorage(AppStorageKeys.poemTitle) var currentPoemTitle = ""
#AppStorage(AppStorageKeys.poemAuthor) var currentPoemAuthor = ""
var body: some View {
VStack(spacing: 10){
Text("App Storage:")
Text(currentPoemTitle)
Text(currentPoemAuthor)
Divider()
Text("Observed Object:")
Text(fetch.poems.first?.title ?? "")
Text(fetch.poems.first?.author ?? "")
Button("Fetch next text") {
fetch.getPoem()
}
}
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}

Getting JSON from a public API to render in a list view in SwiftUI

I'm trying to get the data from a Rest Api to download and render in a list view in SwiftUI.
I think I manage to get the JSON to download and assign to all the relevant Structs correctly but nothing displays in the list view on the simulator when I go to build it.
I'm not even sure I need to have the 'Enum CodingKeys in there.
Can anyone point out where I may be going wrong?
import Foundation
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var fetcher = LaunchDataFetcher()
var body: some View {
VStack {
List(fetcher.launches) { launch in
VStack (alignment: .leading) {
Text(launch.mission_name)
Text(launch.details)
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
}
public class LaunchDataFetcher: ObservableObject {
#Published var launches = [launch]()
init(){
load()
}
func load() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([launch].self, from: d)
DispatchQueue.main.async {
self.launches = decodedLists
}
}else {
print("No Data")
}
} catch {
print ("Error")
}
}.resume()
}
}
struct launch: Codable {
public var flight_number: Int
public var mission_name: String
public var details: String
enum CodingKeys: String, CodingKey {
case flight_number = "flight_number"
case mission_name = "mission_name"
case details = "details"
}
}
// Now conform to Identifiable
extension launch: Identifiable {
var id: Int { return flight_number }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
First of all, I try to find which error of your code and recognize the launch.details of response data somewhere have null. So that I just mark this property Optional and it work.
For more details, you can refer below code :
struct launch: Codable {
public var flight_number: Int
public var mission_name: String
public var details: String?
enum CodingKeys: String, CodingKey {
case flight_number = "flight_number"
case mission_name = "mission_name"
case details = "details"
}
}
At catch line, get the error message to know exactly what's happen
func load() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([launch].self, from: d)
DispatchQueue.main.async {
print(decodedLists)
self.launches = decodedLists
}
}else {
print("No Data")
}
} catch let parsingError {
print ("Error", parsingError)
}
}.resume()
}
Hope this help!
Any messages in console? I think you need to add NSAppTransportSecurity>NSAllowsArbitraryLoads>YES in your .plist
Transport security has blocked a cleartext HTTP
With the help from Trai Nguyen, I managed to get the simulator to display the data.
All I was missing was to ensure the variable named 'details' had an optional property (by adding on the "?") - like so:
public var details: String?
and then making sure when I render the text in my view, I had to give it a value to insert if the return is null - like so:
Text(launch.details ?? "No Data Found")
Here is the complete code that works for me:
import Foundation
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var fetcher = LaunchDataFetcher()
var body: some View {
VStack {
List(fetcher.launches) { launch in
VStack (alignment: .leading) {
Text(launch.mission_name)
Text(launch.details ?? "No Data Found")
.font(.system(size: 11))
.foregroundColor(Color.gray)
}
}
}
}
}
public class LaunchDataFetcher: ObservableObject {
#Published var launches = [launch]()
init(){
load()
}
func load() {
let url = URL(string: "https://api.spacexdata.com/v3/launches")!
URLSession.shared.dataTask(with: url) {(data,response,error) in
do {
if let d = data {
let decodedLists = try JSONDecoder().decode([launch].self, from: d)
DispatchQueue.main.async {
print(decodedLists)
self.launches = decodedLists
}
}else {
print("No Data")
}
} catch let parsingError {
print ("Error", parsingError)
}
}.resume()
}
}
struct launch: Codable {
public var flight_number: Int
public var mission_name: String
public var details: String?
}
/// Now conform to Identifiable
extension launch: Identifiable {
var id: Int { return flight_number }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Resources