I'm building an app, which displays PDFs, but I don't know how to display a PDF file using SwiftUI. I found tutorials on how to display a PDF file using UIKit, but there are no tutorials about SwiftUI. Can anyone help me?
I'm also trying to do that using MVVM design pattern. If there's someone, who can help me, I will be extremely grateful!
Code:
HomeView.swift
import SwiftUI
struct HomeView: View {
var deeds: [Deed] = deedsData
var body: some View {
NavigationView {
List(deeds) { item in
Button(action: {
}) {
HStack {
Image(systemName: "doc.fill")
Text(item.title)
}
}
}
.navigationTitle("title")
}
}
}
struct HomeView_Previews: PreviewProvider {
static var previews: some View {
HomeView(deeds: deedsData)
}
}
DeedModel.swift
import SwiftUI
struct Deed: Identifiable {
var id = UUID()
var title: String
var URL: String
}
let deedsData: [Deed] = [
Deed(title: NSLocalizedString("civilCode", comment: "Civil code"), URL: "https://isap.sejm.gov.pl/isap.nsf/download.xsp/WDU19640160093/U/D19640093Lj.pdf"),
Deed(title: NSLocalizedString("penalCode", comment: "Penal code"), URL: "https://isap.sejm.gov.pl/isap.nsf/download.xsp/WDU19970880553/U/D19970553Lj.pdf"),
Deed(title: NSLocalizedString("civilProcedureCode", comment: "Code of civil procedure"), URL: "https://isap.sejm.gov.pl/isap.nsf/download.xsp/WDU19640430296/U/D19640296Lj.pdf"),
Deed(title: NSLocalizedString("familyAndGuardianshipCode", comment: "Family and guardianship code"), URL: "http://isap.sejm.gov.pl/isap.nsf/download.xsp/WDU19640090059/U/D19640059Lj.pdf"),
Deed(title: NSLocalizedString("laborCode", comment: "Labor code"), URL: "https://isap.sejm.gov.pl/isap.nsf/download.xsp/WDU19740240141/U/D19740141Lj.pdf"),
]
Anyone knows how can I do that in MVVM pattern?
To display a PDF with Apple-only frameworks, you'll need to use UIKit, via a UIViewRepresentable:
import PDFKit
import SwiftUI
struct PDFKitRepresentedView: UIViewRepresentable {
typealias UIViewType = PDFView
let data: Data
let singlePage: Bool
init(_ data: Data, singlePage: Bool = false) {
self.data = data
self.singlePage = singlePage
}
func makeUIView(context _: UIViewRepresentableContext<PDFKitRepresentedView>) -> UIViewType {
// Create a `PDFView` and set its `PDFDocument`.
let pdfView = PDFView()
pdfView.document = PDFDocument(data: data)
pdfView.autoScales = true
if singlePage {
pdfView.displayMode = .singlePage
}
return pdfView
}
func updateUIView(_ pdfView: UIViewType, context _: UIViewRepresentableContext<PDFKitRepresentedView>) {
pdfView.document = PDFDocument(data: data)
}
}
then PDFKitRepresentedView can be used in your view hierarchy.
This takes a Data object as input, so you'll need to convert those URL objects you have to Data via a network call first. So, you might do something like:
#State var data : Data?
...
.onAppear {
self.data = try? Data(contentsOf: url)
}
Keep in mind this is vastly simplified -- you'll want to do some error handling, etc. Might want to do some searching on SwiftUI network calls.
Related
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 ?? "-")
}
}
been going back and forth for 2 days trying to figure this out before posting and still hitting a wall.
Created an API specific class, a ViewModel, and a View and trying to shuttle data back and forth and while I see the API call is successful and I decode it without issue on logs, it never reflects on the UI or View.
As far as I see I appear to be trying to access the data before it's actually available. All help greatly appreciated!
API Class:
import Combine
import Foundation
class CrunchbaseApi:ObservableObject
{
#Published var companies:[Company] = [Company]()
#Published var singleCompany:Company?
func retrieve(company:String) async
{
let SingleEntityURL:URL = URL(string:"https://api.crunchbase.com/api/v4/entities/organizations/\(company)?card_ids=fields&user_key=**********REMOVED FOR SECURITY*****************")!
let task = URLSession.shared.dataTask(with:SingleEntityURL){ data, response, error in
let decoder = JSONDecoder()
if let data = data{
do {
self.singleCompany = try decoder.decode(Company.self, from: data)
} catch {
print(error.localizedDescription)
}
}
}
task.resume()
}
func retrieveCompanyList()
{
//declare
}
}
ViewModel:
import Combine
import Foundation
class CompanyViewModel: ObservableObject
{
var crunchbase:CrunchbaseApi = CrunchbaseApi()
#Published var singleCompany:Company?
func retrieveCompany(company:String) async
{
await self.crunchbase.retrieve(company: company)
self.singleCompany = crunchbase.singleCompany
}
}
View:
import SwiftUI
struct CompanyView: View
{
#State var companyViewModel:CompanyViewModel = CompanyViewModel()
var body: some View
{
NavigationView
{
VStack
{
Text("Company ID: \(companyViewModel.singleCompany?.id ?? "NOTHING")")
// Text("Company Name: \(companyViewModel.companyName)")
// Text("Company Summary: \(companyViewModel.companyDescription)")
// Text("Logo URL: \(companyViewModel.companyLogoURL)")
}.navigationTitle("Company")
}
}
}
Your assumption about accessing the data to early is correct. But there are more things going on here.
just declaring a function async like your retrieve func doesn´t make it async.
using a nested Observable class with #Published will not update the view
Observable classes should have either an #StateObject or an #ObservableObject property wrapper. Depending on if the class is injected or created in the view
Possible solution:
Move the function into the viewmodel:
class CompanyViewModel: ObservableObject
{
#Published var singleCompany:Company?
func retrieve(company:String)
{
let SingleEntityURL:URL = URL(string:"https://api.crunchbase.com/api/v4/entities/organizations/\(company)?card_ids=fields&user_key=**********REMOVED FOR SECURITY*****************")!
let task = URLSession.shared.dataTask(with:SingleEntityURL){ data, response, error in
let decoder = JSONDecoder()
if let data = data{
do {
self.singleCompany = try decoder.decode(Company.self, from: data)
} catch {
print(error.localizedDescription)
}
}
}
task.resume()
}
}
Change the View to hold the viewmodel as #StateObject, also add an .onApear modifier to load the data:
struct CompanyView: View
{
#StateObject var companyViewModel:CompanyViewModel = CompanyViewModel()
var body: some View
{
NavigationView
{
VStack
{
Text("Company ID: \(companyViewModel.singleCompany?.id ?? "NOTHING")")
// Text("Company Name: \(companyViewModel.companyName)")
// Text("Company Summary: \(companyViewModel.companyDescription)")
// Text("Logo URL: \(companyViewModel.companyLogoURL)")
}.navigationTitle("Company")
.onAppear {
companyViewModel.retrieve(company: "whatever")
}
}
}
}
I want to make it so you can favourite a "landmark" in one view (LandmarkDetail), and access a list of all the "landmarks" in another view with the ones I've favourited highlighted. First I used "#AppStorrage" but I was told too to use Core Data for it instead. So far I have the favourite button working in the LandmarkDetail view with "#AppStorage" but apparently I need to change that so it uses Core Data.
I've look around to get an understanding of how to do it with Core Data but I could really use a helping hand if anyone can help. I've already seen and read some tutorials about core data and how to set it up, but I can't find anything for my specific problem where I pull in data from a JSON and I need Core Data to handle the favourite feature.
Here is my code for the favourite button
struct FavoriteButton: View {
#AppStorage ("isFavorite") var isFavorite: Bool = false
var body: some View {
Button {
isFavorite.toggle()
} label: {
Label("Toggle Favorite", systemImage: isFavorite ? "star.fill" : "star")
.labelStyle(.iconOnly)
.foregroundColor(isFavorite ? .yellow : .gray)
}
}
}
Code from the landmark detail view
struct LandmarkDetail: View {
var body: some View {
ScrollView {
VStack(alignment: .leading) {
HStack {
Text(landmark.name)
.font(.title)
FavoriteButton()
}
}
}
}
}
Code for the rows in the list view
This is the one not working yet, so far it just pulls the data from a JSON.
MODEL
import Foundation
import SwiftUI
import CoreLocation
struct Landmark: Hashable, Codable, Identifiable {
var id: Int
var name: String
var park: String
var state: String
var description: String
var isFavorite: Bool
var isFeatured: Bool
var category: Category
enum Category: String, CaseIterable, Codable {
case lakes = "Lakes"
case rivers = "Rivers"
case mountains = "Mountains"
}
private var imageName: String
var image: Image{
Image(imageName)
}
var featureImage: Image? {
isFeatured ? Image(imageName + "_feature") : nil
}
private var coordinates: Coordinates
var locationCoordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: coordinates.latitude,
longitude: coordinates.longitude)
}
struct Coordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}
}
import Foundation
import Combine
final class ModelData: ObservableObject {
#Published var landmarks: [Landmark] = load("landmarkData.json")
var hikes: [Hike] = load("hikeData.json")
#Published var profile = Profile.default
var features: [Landmark] {
landmarks.filter { $0.isFeatured }
}
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarks,
by: { $0.category.rawValue }
)
}
}
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
First of all you need to create a .xcdatamodeld file named Landmarks. You can create it by pressing right button on the principal folder of your project and searching Data Model. After you need to create a new Entity named Landmark. You can add attributes showed in your model like id, name, park, etc... with their types. After you need to create a new Swift file in which you can create you Core Data Controller like this:
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "Landmarks")
init() {
container.loadPersistentStores { description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
}
}
}
}
Successively, you need to add to your LandmarksApp.swift file the following code:
struct LandmarksApp: App {
#StateObject private var dataController = DataController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}
Continue adding this to your LandmarkDetail view:
struct LandmarkDetail: View {
#Environment(\.managedObjectContext) var moc
var body: some View {
ScrollView {
VStack(alignment: .leading) {
HStack {
Text(landmark.name)
.font(.title)
FavoriteButton()
}
}
}
}
}
To create a new item and add to Core Data you can write:
let landmark = Landmark(context: moc)
landmark.id = id
landmark.name = name
landmark.park = park
etc...
try? moc.save()
For your JSON data you can create a function that convert all JSON data in Core Data following these steps.
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"))
}
}
So I am learning how to use SwiftUI with a json api. I am currently generating views through a list and a ForEach Loop. I am wondering how I can make it so that it only generates the first, say 10 elements in the posts array, instead of generating the entire list from the api. Basically I want to use RandomElement() to display 10 random posts from the entire array. I am just beginning here and learning so any help woul dbe appreicated.
Below is the code for my main view that is displaying the list
import SwiftUI
struct postList: View {
//state variable of the posts
#State var posts: [Post] = []
var array = [Post]()
var body: some View {
List {
ForEach(posts) { post in
VStack(alignment: .leading) {
Text(post.title)
.font(.headline)
Text(post.body)
.font(.callout)
}
.padding()
}
}
.onAppear() {
Api().getPosts { (posts) in
self.posts = posts
}
}
}
}
struct postList_Previews: PreviewProvider {
static var previews: some View {
postList()
}
}
Down here is the Data file I use to retrieve the json data
import SwiftUI
struct Post: Codable, Identifiable {
let id = UUID()
var title: String
var body: String
}
class Api {
func getPosts(completion: #escaping ([Post]) -> ()) {
guard let url = URL(string: "http://jsonplaceholder.typicode.com/posts") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
let posts = try! JSONDecoder().decode([Post].self, from: data!)
DispatchQueue.main.async {
completion(posts)
}
}
.resume()
}
}