Reputation: 145
I am writing an app that shows users large pictures that they can flick through and zoom in, similar to the photos app or a book. I am trying to write this with SwiftUI. I have been following the SwiftUI tutorials on apples site and I have adapted the code to read a json file to create an array of "Imageslides" - a way to hold the images and information about them. I have then used PageView and PageViewController to display the ContentView> ContentView displays the image and some buttons that show text about the images I am displaying. My problem is when I swipe through the pages the memory goes up and up until the app crashes. Now, if I was using UIKit I could use
if let imgPath = Bundle.main.path(forResource: name, ofType: nil)
{
return UIImage(contentsOfFile: imgPath)
}
But I can't see how to use contentsOfFile using Image in swiftUI, or another way that will let the images get out of memory when not needed. I have the images in a resources folder, not in the assets library. Thanks for your help.
//data.swift - reading from json, mainly from apple's tutorials
import Foundation
import UIKit
import SwiftUI
let imageData: [Imageslide] = load("imageData.json")
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)")
}
}
final class ImageStore {
typealias _ImageDictionary = [String: CGImage]
fileprivate var images: _ImageDictionary = [:]
fileprivate static var scale = 2
static var shared = ImageStore()
func image(name: String) -> Image {
let index = _guaranteeImage(name: name)
return Image(images.values[index], scale: CGFloat(ImageStore.scale), label: Text(name))
}
static func loadImage(name: String) -> CGImage {
guard
let url = Bundle.main.url(forResource: name, withExtension: "png"),
let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
else {
fatalError("Couldn't load image \(name).jpg from main bundle.")
}
return image
}
fileprivate func _guaranteeImage(name: String) -> _ImageDictionary.Index {
if let index = images.index(forKey: name) { return index }
images[name] = ImageStore.loadImage(name: name)
return images.index(forKey: name)!
}
}
//imageslide.swift - abstract for holding my image slide and button info
import SwiftUI
struct buttonInfo: Hashable, Codable{
var text: String? = nil
var coords: [Int]? = nil
public enum CodingKeys : String, CodingKey {
case text, coords
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.text = try container.decodeIfPresent(String.self, forKey: .text)
self.coords = try container.decodeIfPresent([Int].self, forKey: .coords)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.text, forKey: .text)
try container.encode(self.coords, forKey: .coords)
}
}
struct Imageslide: Hashable, Codable, Identifiable {
public var imagename: String
public var id: Int
public var noButtons: Int
public var buttoninfo: [buttonInfo]? = nil
public enum CodingKeys : String, CodingKey {
case imagename, id, noButtons, buttoninfo
case imageslide = "Imageslide"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let imageslide = try container.nestedContainer(keyedBy:
CodingKeys.self, forKey: .imageslide)
self.imagename = try imageslide.decode(String.self, forKey: .imagename)
self.id = try imageslide.decode(Int.self, forKey: .id)
self.noButtons = try imageslide.decode(Int.self, forKey: .id)
self.buttoninfo = try imageslide.decodeIfPresent([buttonInfo].self, forKey: .buttoninfo)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var imageslide = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .imageslide)
try imageslide.encode(self.imagename, forKey: .imagename)
try imageslide.encode(self.id, forKey: .id)
try imageslide.encode(self.noButtons, forKey: .noButtons)
try imageslide.encode(self.buttoninfo, forKey: .buttoninfo)
}
}
extension Imageslide {
var image: Image {
ImageStore.shared.image(name: imagename)
}
}
struct ButtonDataStore: Codable{
var buttonData: [buttonInfo]
}
//contentview.swift - used to show the images and buttons
import SwiftUI
struct ContentView: View {
var imageslide: Imageslide
@State private var button1Visible = true
@State var scale: CGFloat = 1.0
var body: some View {
ZStack {
Image(uiImage: (UIImage(named: imageslide.imagename)!))
// imageslide.image
// Image(imageslide.imagename)
.resizable()
.frame(width: 2732/2, height: 2048/2)
//Pinch to zoom code goes here
.background(/*@START_MENU_TOKEN@*/Color.black/*@END_MENU_TOKEN@*/)
.edgesIgnoringSafeArea(.all)
.statusBar(hidden: true)
if (imageslide.noButtons != 0)
{
if imageslide.buttoninfo != nil
{
ForEach(imageslide.buttoninfo!, id: \.self) {count in
ButtonView(text: count.text!, xcoord: count.coords![0], ycoord: count.coords![1], xcoordo: count.coords![2], ycoordo: count.coords![3])
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
//ContentView(imageslide: imageData[0])
ContentView(imageslide: imageData[1]).previewLayout(
.fixed(width: 2732/2, height: 2048/2)
)
}
}
}
//PageViewController.swift
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
@Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[controllers[currentPage]], direction: .forward, animated: true)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController?
{
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.controllers.last
}
return parent.controllers[index - 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController?
{
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == parent.controllers.count {
return parent.controllers.first
}
return parent.controllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.controllers.firstIndex(of: visibleViewController)
{
parent.currentPage = index
}
}
}
}
//pageview.swift
import SwiftUI
struct PageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
@State var currentPage = 0
init(_ views: [Page]) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
}
var body: some View {
PageViewController(controllers: viewControllers, currentPage: $currentPage)
}
}
struct PageView_Previews: PreviewProvider {
static var previews: some View {
PageView(imageData.map {ContentView(imageslide: $0)})
}
}
Sample of the JSON I am using.
{
"Imageslide":{
"imagename":"MOS_SHB_1",
"id":1001,
"noButtons":0
}
},
{
"Imageslide":{
"imagename":"MOS_SHB_2",
"id":1002,
"noButtons":0
}
},
{
"Imageslide":{
"imagename":"MOS_SHB_3",
"id":1003,
"noButtons":1,
"buttoninfo":[
{
"text":"The two halves of the arch touched for the first time. Workers riveted both top and bottom sections of the arch together, and the arch became self-supporting, allowing the support cables to be removed. On 20 August 1930 the joining of the arches was celebrated by flying the flags of Australia and the United Kingdom from the jibs of the creeper cranes.",
"coords":[
-150,
220,
200,
200
]
}
]
}
},
Upvotes: 2
Views: 818
Reputation: 257503
Create UIImage
as before
let imageModel = UIImage(contentsOfFile: imgPath)
and use it in body
as a model for Image
view
Image(uiImage: imageModel ?? UIImage())
Upvotes: 2