diff --git a/Modules/Sources/WordPressKit/StatsFileDownloadsTimeIntervalData.swift b/Modules/Sources/WordPressKit/StatsFileDownloadsTimeIntervalData.swift index 28eb5ba1d926..bbf287bf1009 100644 --- a/Modules/Sources/WordPressKit/StatsFileDownloadsTimeIntervalData.swift +++ b/Modules/Sources/WordPressKit/StatsFileDownloadsTimeIntervalData.swift @@ -44,7 +44,7 @@ extension StatsFileDownloadsTimeIntervalData: StatsTimeIntervalData { public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String: AnyObject]) { guard let unwrappedDays = type(of: self).unwrapDaysDictionary(jsonDictionary: jsonDictionary), - let fileDownloadsDict = unwrappedDays["files"] as? [[String: AnyObject]] + let fileDownloadsDict = Bamboozled.parseArray(unwrappedDays["files"]) else { return nil } diff --git a/Modules/Sources/WordPressKit/StatsSearchTermTimeIntervalData.swift b/Modules/Sources/WordPressKit/StatsSearchTermTimeIntervalData.swift index af940dc313b2..2d83ada559eb 100644 --- a/Modules/Sources/WordPressKit/StatsSearchTermTimeIntervalData.swift +++ b/Modules/Sources/WordPressKit/StatsSearchTermTimeIntervalData.swift @@ -45,7 +45,7 @@ extension StatsSearchTermTimeIntervalData: StatsTimeIntervalData { let totalSearchTerms = unwrappedDays["total_search_terms"] as? Int, let hiddenSearchTerms = unwrappedDays["encrypted_search_terms"] as? Int, let otherSearchTerms = unwrappedDays["other_search_terms"] as? Int, - let searchTermsDict = unwrappedDays["search_terms"] as? [[String: AnyObject]] + let searchTermsDict = Bamboozled.parseArray(unwrappedDays["search_terms"]) else { return nil } diff --git a/Modules/Sources/WordPressKit/StatsServiceRemoteV2.swift b/Modules/Sources/WordPressKit/StatsServiceRemoteV2.swift index c2bc64d52e2f..067c8fdc7739 100644 --- a/Modules/Sources/WordPressKit/StatsServiceRemoteV2.swift +++ b/Modules/Sources/WordPressKit/StatsServiceRemoteV2.swift @@ -444,7 +444,37 @@ extension StatsTimeIntervalData { } return nil } +} +enum Bamboozled { + /// Sometimes PHP returns a dictionary with numbers as keys instead of an + /// actual array. This fixes it. + static func parseArray(_ object: AnyObject?) -> [[String: AnyObject]]? { + guard let object else { + return nil + } + if let array = object as? [[String: AnyObject]] { + return array + } + if let dictionary = object as? [String: [String: AnyObject]] { + return dictionary.sorted { lhs, rhs in + if let lhs = Int(lhs.key), let rhs = Int(rhs.key) { + return lhs < rhs + } + return lhs.key.compare(rhs.key, options: .numeric) == .orderedAscending + }.map { + $0.value + } + } + if let dictionary = object as? [Int: [String: AnyObject]] { + return dictionary.sorted { lhs, rhs in + lhs.key < rhs.key + }.map { + $0.value + } + } + return nil + } } // We'll bring `StatsPeriodUnit` into this file when the "old" `WPStatsServiceRemote` gets removed. diff --git a/Modules/Sources/WordPressKit/StatsTopAuthorsTimeIntervalData.swift b/Modules/Sources/WordPressKit/StatsTopAuthorsTimeIntervalData.swift index 9d6ace2a97a0..a4ceab803c64 100644 --- a/Modules/Sources/WordPressKit/StatsTopAuthorsTimeIntervalData.swift +++ b/Modules/Sources/WordPressKit/StatsTopAuthorsTimeIntervalData.swift @@ -70,7 +70,7 @@ extension StatsTopAuthorsTimeIntervalData: StatsTimeIntervalData { public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String: AnyObject]) { guard let unwrappedDays = type(of: self).unwrapDaysDictionary(jsonDictionary: jsonDictionary), - let authors = unwrappedDays["authors"] as? [[String: AnyObject]] + let authors = Bamboozled.parseArray(unwrappedDays["authors"]) else { return nil } @@ -87,7 +87,7 @@ extension StatsTopAuthor { let name = jsonDictionary["name"] as? String, let views = jsonDictionary["views"] as? Int, let avatar = jsonDictionary["avatar"] as? String, - let posts = jsonDictionary["posts"] as? [[String: AnyObject]] + let posts = Bamboozled.parseArray(jsonDictionary["posts"]) else { return nil } diff --git a/Modules/Sources/WordPressKit/StatsTopClicksTimeIntervalData.swift b/Modules/Sources/WordPressKit/StatsTopClicksTimeIntervalData.swift index e270df463aa5..002be33b6aad 100644 --- a/Modules/Sources/WordPressKit/StatsTopClicksTimeIntervalData.swift +++ b/Modules/Sources/WordPressKit/StatsTopClicksTimeIntervalData.swift @@ -50,7 +50,7 @@ extension StatsTopClicksTimeIntervalData: StatsTimeIntervalData { public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String: AnyObject]) { guard let unwrappedDays = type(of: self).unwrapDaysDictionary(jsonDictionary: jsonDictionary), - let clicks = unwrappedDays["clicks"] as? [[String: AnyObject]] + let clicks = Bamboozled.parseArray(unwrappedDays["clicks"]) else { return nil } diff --git a/Modules/Sources/WordPressKit/StatsTopCountryTimeIntervalData.swift b/Modules/Sources/WordPressKit/StatsTopCountryTimeIntervalData.swift index 9ca0703e9a1d..18f0685824b1 100644 --- a/Modules/Sources/WordPressKit/StatsTopCountryTimeIntervalData.swift +++ b/Modules/Sources/WordPressKit/StatsTopCountryTimeIntervalData.swift @@ -43,7 +43,7 @@ extension StatsTopCountryTimeIntervalData: StatsTimeIntervalData { public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String: AnyObject]) { guard let unwrappedDays = type(of: self).unwrapDaysDictionary(jsonDictionary: jsonDictionary), - let countriesViews = unwrappedDays["views"] as? [[String: AnyObject]] + let countriesViews = Bamboozled.parseArray(unwrappedDays["views"]) else { return nil } diff --git a/Modules/Sources/WordPressKit/StatsTopPostsTimeIntervalData.swift b/Modules/Sources/WordPressKit/StatsTopPostsTimeIntervalData.swift index 6a3ad2ff9f7f..f6edaca0ba68 100644 --- a/Modules/Sources/WordPressKit/StatsTopPostsTimeIntervalData.swift +++ b/Modules/Sources/WordPressKit/StatsTopPostsTimeIntervalData.swift @@ -28,7 +28,7 @@ extension StatsTopPostsTimeIntervalData: StatsTimeIntervalData { public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String: AnyObject]) { guard let unwrappedDays = type(of: self).unwrapDaysDictionary(jsonDictionary: jsonDictionary), - let posts = unwrappedDays["postviews"] as? [[String: AnyObject]] + let posts = Bamboozled.parseArray(unwrappedDays["postviews"]) else { return nil } diff --git a/Modules/Sources/WordPressKit/StatsTopReferrersTimeIntervalData.swift b/Modules/Sources/WordPressKit/StatsTopReferrersTimeIntervalData.swift index 03c1bb98959a..262d412c842d 100644 --- a/Modules/Sources/WordPressKit/StatsTopReferrersTimeIntervalData.swift +++ b/Modules/Sources/WordPressKit/StatsTopReferrersTimeIntervalData.swift @@ -51,7 +51,7 @@ extension StatsTopReferrersTimeIntervalData: StatsTimeIntervalData { public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String: AnyObject]) { guard let unwrappedDays = type(of: self).unwrapDaysDictionary(jsonDictionary: jsonDictionary), - let referrers = unwrappedDays["groups"] as? [[String: AnyObject]] + let referrers = Bamboozled.parseArray(unwrappedDays["groups"]) else { return nil } diff --git a/Modules/Sources/WordPressKit/StatsTopVideosTimeIntervalData.swift b/Modules/Sources/WordPressKit/StatsTopVideosTimeIntervalData.swift index 0690474bc261..72ae9467ab50 100644 --- a/Modules/Sources/WordPressKit/StatsTopVideosTimeIntervalData.swift +++ b/Modules/Sources/WordPressKit/StatsTopVideosTimeIntervalData.swift @@ -48,7 +48,7 @@ extension StatsTopVideosTimeIntervalData: StatsTimeIntervalData { let unwrappedDays = type(of: self).unwrapDaysDictionary(jsonDictionary: jsonDictionary), let totalPlayCount = unwrappedDays["total_plays"] as? Int, let otherPlays = unwrappedDays["other_plays"] as? Int, - let videos = unwrappedDays["plays"] as? [[String: AnyObject]] + let videos = Bamboozled.parseArray(unwrappedDays["plays"]) else { return nil } diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 4d470ef8574d..aaec9a1a4cb4 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,6 +1,11 @@ 26.4 ----- +26.3.1 +------ +* [*] Fix an issue with Top Posts not loading for some users [#24860] +* [*] Fix crash in Reader Article view [#24857] +* [*] Fix more minor visual glitches on iOS 26 [#24858] 26.3 ----- diff --git a/WordPress/Classes/ViewRelated/Post/Controllers/AbstractPostListViewController.swift b/WordPress/Classes/ViewRelated/Post/Controllers/AbstractPostListViewController.swift index 3f33943d12d2..ca3e58a7b2d0 100644 --- a/WordPress/Classes/ViewRelated/Post/Controllers/AbstractPostListViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Controllers/AbstractPostListViewController.swift @@ -198,7 +198,11 @@ class AbstractPostListViewController: UIViewController, searchResultsViewController.configure(searchController, self as? InteractivePostViewDelegate) if #available(iOS 26, *) { - navigationItem.preferredSearchBarPlacement = .integrated + if tabBarController?.isTabBarHidden == false { + navigationItem.preferredSearchBarPlacement = .integratedButton + } else { + navigationItem.preferredSearchBarPlacement = .integrated + } } definesPresentationContext = true @@ -850,28 +854,34 @@ class AbstractPostListViewController: UIViewController, // MARK: - Misc private func showRefreshingIndicator() { - guard navigationItem.titleView == nil else { - return - } + // TODO: Design a better way to show it that doesn't affect the navigation bar - let spinner = UIActivityIndicatorView(style: .medium) - spinner.startAnimating() - spinner.tintColor = .secondaryLabel - spinner.transform = .init(scaleX: 0.8, y: 0.8) + if #unavailable(iOS 26) { + guard navigationItem.titleView == nil else { + return + } - let titleView = UILabel() - titleView.text = Strings.updating + "..." - titleView.font = UIFont.preferredFont(forTextStyle: .headline) - titleView.textColor = UIColor.secondaryLabel + let spinner = UIActivityIndicatorView(style: .medium) + spinner.startAnimating() + spinner.tintColor = .secondaryLabel + spinner.transform = .init(scaleX: 0.8, y: 0.8) - let stack = UIStackView(arrangedSubviews: [spinner, titleView]) - stack.spacing = 8 + let titleView = UILabel() + titleView.text = Strings.updating + "..." + titleView.font = UIFont.preferredFont(forTextStyle: .headline) + titleView.textColor = UIColor.secondaryLabel - navigationItem.titleView = stack + let stack = UIStackView(arrangedSubviews: [spinner, titleView]) + stack.spacing = 8 + + navigationItem.titleView = stack + } } private func hideRefreshingIndicator() { - navigationItem.titleView = nil + if #unavailable(iOS 26) { + navigationItem.titleView = nil + } } private func setFooterHidden(_ isHidden: Bool) { diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift index 121034f1424e..68e8074a21e8 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailViewController.swift @@ -87,6 +87,8 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { private lazy var toolbar = ReaderDetailToolbar() private var lastContentOffset: CGFloat = 0 + private var toolbarUpdateTimer: Timer? + /// Likes summary view private let likesSummary: ReaderDetailLikesView = .loadFromNib() @@ -136,6 +138,8 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { /// has a comment anchor fragment. private var hasAutomaticallyTriggeredCommentAction = false + private var isToolbarHidden = false + // Reader customization model private lazy var displaySettingStore: ReaderDisplaySettingStore = { let store = ReaderDisplaySettingStore() @@ -445,6 +449,7 @@ class ReaderDetailViewController: UIViewController, ReaderDetailView { deinit { scrollObserver?.invalidate() + toolbarUpdateTimer?.invalidate() NotificationCenter.default.removeObserver(self) } @@ -900,8 +905,23 @@ extension ReaderDetailViewController: UIScrollViewDelegate { layoutHeroView() } + private func setNeedsToolbarHidden(_ isHidden: Bool) { + // Debounce to prevent it from quickly switching between states when + // on the edge of the scroll threshold. + toolbarUpdateTimer?.invalidate() + toolbarUpdateTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { [weak self] _ in + self?.setToolbarHidden(isHidden, animated: true) + } + } + private func setToolbarHidden(_ isHidden: Bool, animated: Bool) { - guard navigationController?.isToolbarHidden != isHidden else { return } // Important + guard scrollView.contentSize.height > view.bounds.height * 2.5 else { + return // No point in briefly hiding it + } + guard isToolbarHidden != isHidden else { + return + } + isToolbarHidden = isHidden navigationController?.setToolbarHidden(isHidden, animated: animated) } } diff --git a/config/Version.public.xcconfig b/config/Version.public.xcconfig index 84cf7b1d87e0..eadb1c318b80 100644 --- a/config/Version.public.xcconfig +++ b/config/Version.public.xcconfig @@ -1,2 +1,2 @@ -VERSION_LONG = 26.3.0.8 -VERSION_SHORT = 26.3 +VERSION_LONG = 26.3.1.0 +VERSION_SHORT = 26.3.1