0

I use my app since a 10 years, and recently I realised that the app consumes a lot of data (in gigabytes within a few days) and it actually does nothing... only sync a few simple records with iCloud database. So I have tried to debug where the app consumes the data, and I simplified everything.

Now my app is just simple brown controller:

import Alamofire
import CoreData
import Foundation
import UIKit
import SwiftSoup
import StoreKit
import WebKit
import Firebase
import SVProgressHUD

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    var window: UIWindow?
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        let window = UIWindow()
        window.rootViewController = UIViewController()
        window.rootViewController?.view.backgroundColor = .brown
        window.backgroundColor = .white
        self.window = window
        self.window?.makeKeyAndVisible()
        return true
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        print("active")
    }
}

And that is all. I commented out everything for debug time. And that is most simplified code as possible. And the app still consumes data in megabytes every time when application did become active (active is printed). Why? Where else I can search for the source of that issue? How do I know it? I go to Settings > Mobile Data > My App - and it increases ~3-10 MB every time when app did become active. Why?

enter image description here

Update

After a big research and long debugging I have discovered that for high data consumption are responsible widgets of my app. I have 5 widgets, and each of them display images from URL the following way:

extension Image {
    init(url: URL?) {
        if let url = url, let imageData = try? Data(contentsOf: url), let uiImage = UIImage(data: imageData) {
            self.init(uiImage: uiImage)
        } else {
            self.init(systemName: "photo.fill")
        }
    }
}

It seem that each time when an app did become active every widget is refreshed. And in total there is 20-30 images displayed from URL.

When I remove the widgets from home screen, or remove widgets from embedded content the issue does not exist any more.

Is there a way to cache it or improve performance?

halfer
  • 19,824
  • 17
  • 99
  • 186
Bartłomiej Semańczyk
  • 59,234
  • 49
  • 233
  • 358
  • 1
    Does the same code, but without all of those external dependencies linked, do the same thing? – jnpdx Jul 17 '23 at 15:52
  • I will try without imports... let me try. – Bartłomiej Semańczyk Jul 17 '23 at 15:53
  • 2
    Not just the imports, but also not linking the libraries. – jnpdx Jul 17 '23 at 15:53
  • Not linked? I dont know... But it is hard to check, because my whole code (hundreds of files) depends on these dependencies. I just do not use that code now while compiling... I am pretty sure. What dependency is suspicious? – Bartłomiej Semańczyk Jul 17 '23 at 15:57
  • I commented out all imports here in AppDelegate and nothing changed. – Bartłomiej Semańczyk Jul 17 '23 at 16:03
  • 1
    I don't think it's a useful debugging hint unless we know if it has to do with the linked libraries or not. The inverse would be to start a new project (without any of the linked code) and see if you still see the memory increase – jnpdx Jul 17 '23 at 16:11
  • 1
    Try to listen to traffic to know what are the calls, to which server... It's hard to tell currently what's happening... – Larme Jul 17 '23 at 16:22
  • It is not memory increase, but data usage... Ok, I will check version with unlinked libraries... need some time for it. – Bartłomiej Semańczyk Jul 17 '23 at 16:24
  • @Larme how to listen to traffic? Is there any way for this? – Bartłomiej Semańczyk Jul 17 '23 at 16:24
  • @jnpdx I have unlinked all libraries, and nothing changed... I attached screenshot in question. – Bartłomiej Semańczyk Jul 17 '23 at 16:39
  • Is this running in Debug or Release mode? – jnpdx Jul 17 '23 at 16:41
  • Start by running this on a device with Instruments (choose "Product>Profile"). Select the Network template. This will show you what your app is sending within its process, which is usually most of your traffic. It excludes things like AVPlayer and some other OS-level network traffic which happens on behalf of your process, but will capture any third-party library traffic. For more, see https://developer.apple.com/documentation/foundation/url_loading_system/analyzing_http_traffic_with_instruments – Rob Napier Jul 17 '23 at 16:50
  • @jnpdx Debug mode on device. – Bartłomiej Semańczyk Jul 17 '23 at 16:51
  • "Charles Proxy" ? "Little Snitch" seems to have a few things, "App Privacy Report", etc. ? – Larme Jul 17 '23 at 16:51
  • 1
    Things like Charles are a bit more powerful (and I personally prefer mitmproxy), and can handle things like out-of-process traffic. But Instruments is where to start. It doesn't require any extra tools, and presents the information very clearly IMO. I'm betting this is a third-party library, and Instruments will likely almost immediately point it out. – Rob Napier Jul 17 '23 at 16:55
  • @Rob I believe the issue here is network use, not memory. (Based on the use of `Settings > Mobile Data > My App` to measure it.) – Rob Napier Jul 17 '23 at 17:53
  • @RobNapier Of course issue is with network use, not memory...;) – Bartłomiej Semańczyk Jul 17 '23 at 18:04
  • @RobNapier - Ah, thanks. – Rob Jul 17 '23 at 18:17
  • @RobNapier I have dound where is the issue and updated the question with latest information. If you know any solution, I'd be grateful. Thank you in advance;) – Bartłomiej Semańczyk Jul 19 '23 at 13:22
  • Use a cache system. I guess Alamofire+Image, SDWebImage, etc might work on widgets, or check them yourself if possible? Also using `Data(contentsOf:)` is not recommended as it's blocking the current thread. – Larme Jul 20 '23 at 16:08
  • @Larme Alamofire+Image and SDWebImage is not for SwiftUI, only for iOS – Bartłomiej Semańczyk Jul 21 '23 at 06:02

2 Answers2

0

There's a reason this extension doesn't exist on Image already. Image describes a view. The system is free to construct it many times, and it's assumed to be cheap to create. Your code refetches the data synchronously (blocking the thread) every time init is called.

The tool you want here is AsyncImage, which is designed for this use case.

AsyncImage does not provide much caching itself (I believe it provides standard HTTP caching if your server handles that correctly), but it won't re-load except when needed. If you want to add more extensive caching, see How can I add caching to AsyncImage.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • No, AsyncImage doesnt work for App Extensions... it works for SwiftUI, but not for App Extensions written with SwiftUI. Check [this](https://stackoverflow.com/questions/75774071/asyncimage-doesnt-work-in-images-loaded-for-ios-widgets-using-swiftui) question – Bartłomiej Semańczyk Jul 19 '23 at 14:04
  • See also https://developer.apple.com/forums/thread/716902. The system is trying to prevent you from doing large fetches from the network from extensions on purpose, and you've found a hack around that intention. The general answer is going to be to provide the image from the app (download it there and share it with the extension). But the linked thread discusses some other hacks. But generally the answer is "don't do this in extensions; let the app do the work; keep extensions very lightweight." – Rob Napier Jul 19 '23 at 14:16
  • Generally you are right, but data is fetched ONLY in extension on demand. What then? I changed it and now save everything tio core data model shared between the app and extensions, but this is not enough, because I still have image urls, not images. My only solution is to fetch image for url, and then save it to persistent store (shared user defaults) and reuse it with next try to fetch image for the same url (urls are unique). – Bartłomiej Semańczyk Jul 19 '23 at 15:51
  • There may not be an answer to this in extensions today. I definitely don't believe there's a straightforward "do it this mechanical way and it'll work." You may be doing it as well as it can be done today. – Rob Napier Jul 19 '23 at 16:00
0

So the simple answer to fix following issue was to add Cache-like tool for images based on url and delivered key:

extension Image {
    init(url: URL?, key: String) {
        let defaults = UserDefaults.shared
        let urlKey = "\(key)_url"
        let dataKey = "\(key)_data"
        guard let url = url else {
            self.init(systemName: "photo.fill")
            return
        }
        
        if let cachedUrl = defaults.string(forKey: urlKey),
           let cachedData = defaults.data(forKey: dataKey),
           let image = UIImage(data: cachedData), cachedUrl == url.absoluteString {
            self.init(uiImage: image)
            return
        }
        
        if let imageData = try? Data(contentsOf: url), let uiImage = UIImage(data: imageData) {
            defaults.setValue(url.absoluteString, forKey: urlKey)
            defaults.setValue(imageData, forKey: dataKey)
            defaults.synchronize()
            self.init(uiImage: uiImage)
        } else {
            self.init(systemName: "photo.fill")
        }
    }
}

Example of usage:

Image(url: URL(string: "{url}", key: "{key}"))
Bartłomiej Semańczyk
  • 59,234
  • 49
  • 233
  • 358