SwiftUI Realm Memory issue. RAM usage keeps on increasing - swiftui

I am downloading over 7000 images that need to be saved to a Realm DB. I am writing to the DB in batches of 100. They are temporarily stored in an Array.
My issue is that as the process goes on, RAM usage keeps on increasing. I already wrapped the activity with autoreleasepool { activity }, but it keeps on happening.
func downloadImages() {
autoreleasepool {
realmQueue.async {
let realm = try! Realm()
print("6.5: Downloading Images")
var x = 0
var imageSavedArray : [ImageSaved] = []
let imageUrls = self.urlPrep()
for url1 in imageUrls {
let imageURL = URL(string: url1)
let placeholder = URL(string: "")
let imageToSave = ImageSaved()
if imageURL != nil {
let data = try? Data(contentsOf: imageURL)
imageToSave.link = url1
imageToSave.data = data
imageSavedArray.append(imageToSave)
} else {
print("Invalid Saving Process for " + url1)
}
if ((x % 100) == 0) {
try! realm.write {
realm.add(imageSavedArray)
}
imageSavedArray = []
}
x += 1
}//end for
}//end queue
} //end autoreleasepool
} //end function
[This will continue until eventually it crashes.][1]
As soon as the process finalizes, the RAM goes back to normal.As you can see, I am resetting the array after writing it to Realm.
Any help with this will be greatly appreciated.

Let me provide an answer using generic code to keep it brief, and ignore the .async for this answer.
Also, this code assumes the number of urls in the array is a multiple of 10 to keep it short.
for i in stride(from: 0, to: urlArray.count, by: 10) {
autoreleasepool(invoking: {
let smallUrlArray = myArray[i...i+9] //get 10 urls, start to fill pool
var imageSavedArray = [ImageSaved]()
for imageUrl in smallUrlArray {
let imageToSave = ...create an ImageSaved object from the image and data
imageSavedArray.append(imageToSave)
}
try! realm.write {
realm.add(imageSavedArray)
}
}) //drain pool
}
In the code, iterate over the array of urls every 10 indexes using stride
Within that outside loop, create an autorelease pool
Within the pool an array of 10 urls is created and then iterated over - within that loop, create the realm objects to save, store in an imageSavedArray, then write them to realm.
At the end of the closure, the pool is drained, meaning anything created inside the autorelease pool is disposed of and memory is freed.
So the memory profile looks like this
/| /|
/ | / |
/ | / |
fill^ ^drain

Related

How to continue performing a task when Apple Watch App is in inactive mode?

This bounty has ended. Answers to this question are eligible for a +100 reputation bounty. Bounty grace period ends in 23 hours.
Bartłomiej Semańczyk is looking for a canonical answer.
This is my code what I do on appear:
import SwiftUI
import CloudKit
#main
struct DemoApp: App {
var isDownloading = false
#Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
// some content
}
.onChange(of: scenePhase) { phase in
if case .active = phase {
Task {
await loadData() // this loads should be done on background thread (I think), that is all.
}
}
}
}
private let container = CKContainer(identifier: "iCloud.pl.myapp.identifier")
private var privateDatabase: CKDatabase {
return container.privateCloudDatabase
}
private func loadData() async {
if !isDownloading {
isDownloading = true
var awaitingChanges = true
var changedRecords = [CKRecord]()
var deletedRecordIDs = [CKRecord.ID]()
let zone = CKRecordZone(zoneName: "fieldservice")
var token: CKServerChangeToken? = nil
do {
while awaitingChanges {
let allChanges = try await privateDatabase.recordZoneChanges(inZoneWith: zone.zoneID, since: token)
let changes = allChanges.modificationResultsByID.compactMapValues { try? $0.get().record }
changes.forEach { _, record in
changedRecords.append(record)
print("Fetching \(changedRecords.count) private records.")
// update ui here with printed info
}
let deletetions = allChanges.deletions.map { $0.recordID }
deletetions.forEach { recordId in
deletedRecordIDs.append(recordId)
print("Fetching \(changedRecords.count) private records.")
// update ui here with printed info
}
token = allChanges.changeToken
awaitingChanges = allChanges.moreComing
}
isDownloading = false
print("in future all records should be saved to core data here")
} catch {
print("error \(error)")
isDownloading = false
}
}
}
}
This is simplified code as much as it can be to better understand the problem.
My Apple Watch when I run the app FIRST TIME it must fetch all cloudkit record changes (but in my opinion it doesn't matter what actually is the task). To make it finished it needs to download ~3k records and it takes ~5-6 minutes on the watch. Downloading is in progress ONLY when an app is in ACTIVE mode. When it changes to INACTIVE (after ~10-11 seconds) it stops downloading and I have to move my wrist to make it ACTIVE again to continue downloading.
I need to change it to not to stop downloading when app is in INACTIVE mode. It should continue downloading until it is finished. While it is being downloaded I update UI with info for example "Fetched 1345 records", "Fetched 1346 records" and so on... When app is inactive it is downloaded and when I make an app ACTIVE I can see the current status of download.
What do I do inside loadData? I simply fetch all CloudKit changes starting with serverChangeToken = nil. It takes ~ 5-6 minutes for ~3k records.

JSON data not copied to structure

This api is getting called correctly and is not falling into the "fetch failed" error line. The ContentView structure contains a standard menu with .onAppear at the bottom. Using the Xcode debugger I can see the data in decodedResponse but not in result. Is it really necessary to use #State results? Some of values in result / UserRates will be immediately pulled out and stored elsewhere. I would also like to use the retrieved date but currently my results definition keeps its non-defined. I'm assuming that to retrieve the data from the structure it is: UserRates.rates[9].key and UserRates.rates[9].value. Trying to read these with a print statement returns an error.
Galen Smith
struct UserRates: Codable {
let rates:[String: Double]
let base: String
let date: String
}
struct ContentView: View {
#State private var results = UserRates(rates: [:], base: "", date: "")
var body: some View {
... standard menu system in body
.onAppear(perform: loadCurrencies)
}
private func loadCurrencies() {
guard let url = URL(string: "https://api.exchangeratesapi.io/latest?base=USD") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(UserRates.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
**self.results = decodedResponse**
}
return
}
}
// problem if we fail into this
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
I had planned to copy 32 String / Double data pairs from decodedResponse to the UserRates structure then find and copy 11 exchange rates using the dictionary key / value method to a rateArray. But since the copy always fails, I decided to use the dictionary key / value method on the decodedResponse structure and copying directly to rateArray skipping the UserRate structure entirely. I may in the future use more exchange rates so that is why I load all available rates from my source. The real purpose of rateArray is to store the exchange rate reciprocals.
Galen

Collection view doesn't reload (Swift)

I'm building an app (in swift). One of the options should be to allow the user to either select multiple photos and upload them or take a photo and create a post with it.
My idea is based the following hierarchy:
New Photo post button pressed -> User is presented with UICollection view controller with all photos from their library and an option to select multiple photos (I'm reusing a cell to populate those collection view with the images from the photo library) and above that should be the Camera cell (the user needs to click on the button to allow access to the camera in order to take a photo). Here's a representation from the Simulator.
I've written the code that gets the photos from library and add them in an array here (I'll show the code for these below).
The problem is when you initially load the app for the first time and try to post, you're being asked to grant access to the photos. Despite of the user's choice, (either they agree or don't) they collection view isn't update.
What it needs to happen - When the user is asked to grant access to Photos and they click "OK", it should reload the data in the collection view, but it doesn't. And when they click on "Don't allow" it should dismiss the entire View controller. Here's my code
class CVController: UICollectionViewController, UINavigationControllerDelegate, UICollectionViewDelegateFlowLayout {
var photosLibraryArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
grabAllPhotosFromPhoneLibrary()
}
// Grab All Photos from Library
func grabAllPhotosFromPhoneLibrary () {
let imageManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = true // synchronous works better when grabbing all images
requestOptions.deliveryMode = .opportunistic
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)] // if false = last image we took would show first
let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
// 1. Are there photos in the library
if fetchResult.count > 0 {
// 2. if fetch.count > 0 it means there's at least 1 photo in the library
for i in 0..<fetchResult.count {
// 3. Cycling through the photos
imageManager.requestImage(for: fetchResult.object(at: i), targetSize: CGSize(width: 200, height: 200), contentMode: .aspectFill, options: requestOptions, resultHandler:
{ image, error in
// 4. Append each image in the array
self.photosLibraryArray.append(image!)
})
}
} else {
print("You've got no photos")
self.collectionView?.reloadData()
}
I tried calling collectionView.reloadData() in the viewWillApear(), viewDidApear(), nothing worked.
class CVController: UICollectionViewController, UINavigationControllerDelegate, UICollectionViewDelegateFlowLayout {
var photosLibraryArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
checkPhotoLibraryPermission()
}
Add this below mentioned function
func checkPhotoLibraryPermission() {
let status = PHPhotoLibrary.authorizationStatus()
switch status {
case .authorized:
//handle authorized status - // call this function here
grabAllPhotosFromPhoneLibrary()
case .denied, .restricted :
//handle denied status
case .notDetermined:
// ask for permissions
PHPhotoLibrary.requestAuthorization() { status in
switch status {
case .authorized:
// call this function here
grabAllPhotosFromPhoneLibrary()
case .denied, .restricted:
// as above
_ = navigationController?.popViewController(animated: true)
case .notDetermined:
// won't happen but still, if you want to handle.
}
}
}
}
You may accordingly make changes in the following function below. As per the need.
// Grab All Photos from Library
func grabAllPhotosFromPhoneLibrary () {
let imageManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = true // synchronous works better when grabbing all images
requestOptions.deliveryMode = .opportunistic
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)] // if false = last image we took would show first
let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
// 1. Are there photos in the library
if fetchResult.count > 0 {
// 2. if fetch.count > 0 it means there's at least 1 photo in the library
for i in 0..<fetchResult.count {
// 3. Cycling through the photos
imageManager.requestImage(for: fetchResult.object(at: i), targetSize: CGSize(width: 200, height: 200), contentMode: .aspectFill, options: requestOptions, resultHandler:
{ image, error in
// 4. Append each image in the array
self.photosLibraryArray.append(image!)
})
}
//Use this reload data here as - you want the data loaded in the array first and then show the data.
self.collectionView?.reloadData()
} else {
print("You've got no photos")
//self.collectionView?.reloadData()
// if you want to hide the view controller when there is no photo. pop the view controller from your navigation controller like this.
_ = navigationController?.popViewController(animated: true)
}

JSQMessagesViewController updating bubble image color on update

I am working with JSQMessagesViewController and have implemented three bubble colors. The extra color is designed to indicate an unapproved message in a moderated chat room.
I am running a Firebase backend and updating an approved flag when the chat message entries changes.
All is going well and the data is being changed real time. The problem is with the chat bubble colors, no matter what I do they will not change.
I've tried invalidating the layout, reloaddata, accessing the cell directly (comes up read only) and nothing seems to change the color other than leaving the chat view and coming back.
messageRef.observe(.childChanged, with: { (snapshot) in
let key = snapshot.key
if let dict = snapshot.value as? [String: AnyObject] {
let approved = (dict["approved"]?.boolValue ?? true)
let indexOfMesage = self.messages.index(where:{$0.key == key})
var message = self.messages[indexOfMesage!]
message.approved = approved
print(message)
self.collectionView.performBatchUpdates({ () -> Void in
self.collectionView.collectionViewLayout.invalidateLayout()
self.collectionView.reloadData()
}, completion:nil)
}
Any help would be appreciated. The code above is just one of many attempts.
Adding my "messageBubbleImageDataForItemAt" call for additional info after response below.
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAt indexPath: IndexPath!) -> JSQMessageBubbleImageDataSource! {
let message = messages[indexPath.item] // 1
if message.messageItem.senderId == senderId { // 2
if (message.approved == true){
return outgoingBubbleImageView
}else{
return outgoingUnnaprovedBubbleImageView
}
}else if (self.superUsers.contains(message.messageItem.senderId)){
return incomingAdminBubbleImageView
}else { // 3
if (message.approved == true){
return incomingBubbleImageView
}else{
return incomingUnnapprovedBubbleImageView
}
}
}
Invalidating the layout will not do anything for the color of the bubble. I would suggest that you modify you messageDataObject all you have to do is conform to the JSQMessageData protocol and I would add a property for a messages approval status.
I am going to assume that you have three different message bubbles defined
something like this
var incomingBubble: JSQMessagesBubbleImage!
var outgoingBubble: JSQMessagesBubbleImage!
var approvedBubble: JSQMessagesBubbleImage!
then in your view did load you actually define them.
incomingBubble = JSQMessagesBubbleImageFactory().incomingMessagesBubbleImage(with: UIColor.jsq_messageBubbleBlue())
outgoingBubble = JSQMessagesBubbleImageFactory().outgoingMessagesBubbleImage(with: UIColor.lightGray)
approvedBubble = JSQMessagesBubbleImageFactory().outgoingMessagesBubbleImage(with: UIColor.red)
Then in the overridden function messageBubbleImageDataForItemAt you should provide logic for which bubble to use.
override func collectionView(_ collectionView: JSQMessagesCollectionView, messageBubbleImageDataForItemAt indexPath: IndexPath) -> JSQMessageBubbleImageDataSource {
//Sent by the current user
if messages[indexPath.item].senderId == self.senderId(){
return outgoingBubble
}
//Check if the message is approved from that property earlier.
if messages[indexPath.item].approved {
return approvedBubble
}
// its just a normal message return incoming message.
return incomingBubble
}
Other wise you need to fetch from firebase at this point if a message is approved or not.
Then calling self.collectionView.reloadData() should update your colors as long as you have pulled the latest from firebase.
I hope this helps you out. Let me know if you have more questions and keep on keeping on. 🖖🏽

Music control at lock screen iPhone

I am developing music app but for lock screen control i am not able to assign time duration and elapsed time
here is my code
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.previousTrackCommand.isEnabled = true;
commandCenter.previousTrackCommand.addTarget(self, action:#selector(home_ViewController.btn_rewind(_:)))
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.nextTrackCommand.addTarget(self, action:#selector(home_ViewController.btn_fast(_:)))
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget(self, action:#selector(home_ViewController.play_player))
commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.addTarget(self, action:#selector(home_ViewController.pause_player))
commandCenter.togglePlayPauseCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.addTarget(self, action:#selector(home_ViewController.togglePlay_Pause))
commandCenter.skipBackwardCommand.isEnabled = false
commandCenter.skipForwardCommand.isEnabled = false
if #available(iOS 9.1, *) {
commandCenter.changePlaybackPositionCommand.isEnabled = true
} else {
// Fallback on earlier versions
return
}
and media info
func setLockInfo()
{
let url = URL(string: song_UrlString)
let data = try? Data(contentsOf: url!)
let art = MPMediaItemArtwork.init(image: UIImage(data: data!)!)
let songInfo :[String : Any] = [MPMediaItemPropertyTitle :st_title,MPMediaItemPropertyArtwork : art]
MPNowPlayingInfoCenter.default().nowPlayingInfo = songInfo
}
I am getting title and image but lock screen is not displaying time
I am using SWIFT 3 to code
It's not displaying the time because you're not telling it to display the time.
To show the playback time, your nowPlayingInfo dictionary needs to include values for the keys:
MPNowPlayingInfoPropertyElapsedPlaybackTime, so it knows what the current time is when you start playing,
MPMediaItemPropertyPlaybackDuration, so it knows what the current time is relative to in the bar, and
MPNowPlayingInfoPropertyPlaybackRate, so it can automatically update the playback time UI without you needing to periodically set the current time.
If you want the playback time bar to be interactive (that is, allow jumping to a different time instead of just displaying the current time, register a changePlaybackPositionCommand with your remote command center.
Swift 3 example with all commands woking, including changePlaybackPositionCommand:
func remotePlayerInit() {
UIApplication.shared.beginReceivingRemoteControlEvents()
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.pauseCommand.addTarget(self, action: #selector(self.pauseSongTouch(_:)))
commandCenter.playCommand.addTarget(self, action: #selector(self.playSongTouch(_:)))
commandCenter.nextTrackCommand.addTarget(self, action: #selector(self.nextSongTouch(_:)))
commandCenter.previousTrackCommand.addTarget(self, action: #selector(self.previousSongTouch(_:)))
commandCenter.changePlaybackPositionCommand.addTarget(self, action: #selector(self.changedThumbSlider(_:)))
setLockInfo()
}
func changedThumbSlider(_ event: MPChangePlaybackPositionCommandEvent) -> MPRemoteCommandHandlerStatus {
audioPlayer.currentTime = event.positionTime
setLockInfo()
return .success
}
func setLockInfo()
{
let image = PlayerVC.songs[PlayerVC.currentSelection].image
let artwork = MPMediaItemArtwork.init(boundsSize: image.size, requestHandler: { (size) -> UIImage in
return image
})
let songInfo: [String: Any] = [MPMediaItemPropertyTitle: PlayerVC.songs[PlayerVC.currentSelection].name,
MPMediaItemPropertyArtwork: artwork,
MPNowPlayingInfoPropertyElapsedPlaybackTime: TimeInterval(audioPlayer.currentTime),
MPNowPlayingInfoPropertyPlaybackRate: 1,
MPMediaItemPropertyPlaybackDuration: audioPlayer.duration,
MPMediaItemPropertyArtist: "Artist"]
MPNowPlayingInfoCenter.default().nowPlayingInfo = songInfo
}