Problem:
UIView updates cause MainViewController to freeze until UI changes are updated. Can I avoid the freezing of the MainViewController using dispatchAsync or is this completely set up wrong?
I have a MainViewController that has a UIView connected as an IBOutlet via storyboard.
class MainViewController: UITableViewController {
@IBOutlet weak var headerView: MettaHeaderView!
func loadCustomView() {
DispatchQueue.main.async {
self.headerView.populateViews()
}
}
}
The UIView Class is set up like this
class MettaHeaderView: UIView {
//MARK: - Properties
weak var delegate: MettaTableVC!
let dataStore = DataStore.shared
//MARK: - Outlets
@IBOutlet weak var cardView: CurvedUIView!
@IBOutlet weak var dayStack: UIStackView!
@IBOutlet weak var plusBtn: UIButton!
@IBOutlet weak var segmentedControl: UISegmentedControl!
@IBOutlet weak var streakLbl: UILabel!
@IBOutlet weak var todayImg: UIImageView!
@IBOutlet weak var todayLbl: UILabel!
@IBOutlet weak var yesterdayImg: UIImageView!
@IBOutlet weak var yesterdayLbl: UILabel!
@IBOutlet weak var twoDayAgoImg: UIImageView!
@IBOutlet weak var twoDayAgoLbl: UILabel!
@IBOutlet weak var threeDayAgoImg: UIImageView!
@IBOutlet weak var threeDayAgoLbl: UILabel!
@IBOutlet weak var fourDayAgoImg: UIImageView!
@IBOutlet weak var fourDayAgoLbl: UILabel!
@IBOutlet weak var fiveDayAgoImg: UIImageView!
@IBOutlet weak var fiveDayAgoLbl: UILabel!
@IBOutlet weak var sixDayAgoImg: UIImageView!
@IBOutlet weak var sixDayAgoLbl: UILabel!
//MARK: - Override Methods
override func layoutMarginsDidChange() {
super.layoutMarginsDidChange()
self.layoutIfNeeded()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.endEditing(true)
}
//MARK: - Methods
func initializeHeader(with delegate: MettaTableVC) {
self.delegate = delegate
styleCardView()
let font = UIFont.systemFont(ofSize: 12)
let selectedFont = UIFont.systemFont(ofSize: 12, weight: .medium)
segmentedControl.setTitleTextAttributes([NSAttributedString.Key.font: font],
for: .normal)
segmentedControl.setTitleTextAttributes([NSAttributedString.Key.font: selectedFont],
for: .selected)
}
func styleCardView() {
self.layoutIfNeeded()
self.layoutSubviews()
cardView.curvedPercent = 0.17
setFontSize()
}
func populateViews() {
let totalDays = self.totalDays()
let dayString = totalDays == 1 ? "Day" : "Days"
self.streakLbl.text = "\(totalDays) \(dayString) with a Session"
self.setDayLbls()
self.setDayImages(with: dataStore.sessions)
}
func totalDays() -> Int {
let grouped = dataStore.sessions.group { $0.dateString }
return grouped.count
}
func setStreak(with sessions: [MeditationSession]) {
streakLbl.text = "\(currentStreak(from: sessions)) day streak"
}
func currentStreak(from sessions: [MeditationSession]) -> Int {
var streak = 0
var lastDate: Date?
let formatter = DateFormatter()
formatter.dateFormat = "MM/dd/yyyy"
for sesh in sessions {
let date = sesh.date.dateValue()
// Check if first day check complete yet
if let last = lastDate {
let difference = daysBetween(start: date, end: last)
// Check if the same day
if difference == 0 {
continue
}
// Check if day before
if difference == 1 {
lastDate = date
streak += 1
} else {
return streak
}
// If first go through
} else {
let difference = daysBetween(start: date, end: Date())
// Check if today
if difference == 0 {
lastDate = date
streak += 1
continue
}
// Check if day before
if difference == 1 {
lastDate = date
streak += 1
} else {
return streak
}
}
}
return streak
}
func daysBetween(start: Date, end: Date) -> Int {
let cal = Calendar(identifier: .gregorian)
let date1 = cal.startOfDay(for: start)
let date2 = cal.startOfDay(for: end)
return Calendar.current.dateComponents([.day], from: date1, to: date2).day!
}
func setDayLbls() {
let lbls: [UILabel] = [sixDayAgoLbl, fiveDayAgoLbl, fourDayAgoLbl, threeDayAgoLbl, twoDayAgoLbl, yesterdayLbl, todayLbl]
let formatter = DateFormatter()
formatter.dateFormat = "EEEE"
let todayString = formatter.string(from: Date())
guard let today = Day(rawValue: todayString.lowercased()) else { return }
let week = past7Days(from: today)
for (index, val) in week.enumerated() {
lbls[index].text = val
}
}
func setFontSize() {
let labels: [UILabel] = [sixDayAgoLbl, fiveDayAgoLbl, fourDayAgoLbl, threeDayAgoLbl, twoDayAgoLbl, yesterdayLbl, todayLbl]
labels.forEach {
let size: CGFloat = ($0.superview?.frame.width)! * 0.42
$0.font = $0.font.withSize(size)
}
}
func past7Days(from day: Day) -> [String] {
var dayStrings: [String] = []
var currentIndex: Int = day.order
let days = Day.allCasesOrdered
for _ in 0..<7 {
print("here")
let day = days[currentIndex]
dayStrings.append(day.letter)
if currentIndex > 0 {
currentIndex -= 1
} else {
currentIndex = 6
}
}
return dayStrings.reversed()
}
func setDayImages(with sessions: [MeditationSession]) {
let dayImgViews: [UIImageView] = [todayImg, yesterdayImg, twoDayAgoImg, threeDayAgoImg, fourDayAgoImg, fiveDayAgoImg, sixDayAgoImg]
var date: Date = Date()
for index in 0...6 {
print(index)
let filtered = sessions.filter { $0.dateString == date.dateString() }
dayImgViews[index].image = filtered.count == 0 ? #imageLiteral(resourceName: "Lotus_Monochromatic") : #imageLiteral(resourceName: "Lotus")
date = Calendar.current.date(byAdding: .day, value: -1, to: date)!
}
}
Calling the function below in the MainViewController freezes the entire VC until the function completes. Is there a way I can avoid this or fix my problem?
self.headerView.populateViews()
Freeze:
Meaning the entire screen is frozen until that function completes.
So I have found that the issue is because of this function but im not sure why the freeze is happening:
func setDayImages(with sessions: [MeditationSession]) {
let dayImgViews: [UIImageView] = [headerView.todayImg, headerView.yesterdayImg, headerView.twoDayAgoImg, headerView.threeDayAgoImg, headerView.fourDayAgoImg, headerView.fiveDayAgoImg, headerView.sixDayAgoImg]
var date: Date = Date()
for index in 0...6 {
print(index)
let filtered = sessions.filter { $0.dateString == date.dateString() }
dayImgViews[index].image = filtered.count == 0 ? #imageLiteral(resourceName: "Lotus_Monochromatic") : #imageLiteral(resourceName: "Lotus")
date = Calendar.current.date(byAdding: .day, value: -1, to: date)!
}
}
The function outlined at the end func setDayImages(with sessions: [MeditationSession])
inserts images inside UIImageViews, which means each image needs to be loaded into memory as a Bitmap, this is quite a heavy operation and you're running it on the Main thread in your MainViewController
.
My solution would be to load the images into UIImage()
outside of the main thread and then assign them on the main thread later. And see if that improves performance. And in general it is advised that calculations unrelated to the UI be performed outside of the main thread using GCD (DispatchQueue.global(.userInitiated).async
for example).
This is my best guess at least without additional performance diagnostics.