Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f5426ee
Remove unused changeLayoutMargins
kean Oct 16, 2024
d5c16e3
Add new pinEdges method
kean Oct 16, 2024
bfcd219
Add pinCenter
kean Oct 16, 2024
58368a8
Add initial ReaderDiscoverHeaderView implementation
kean Oct 16, 2024
36f9ff9
Remove readerDiscoverEndpoint FF
kean Oct 16, 2024
3ea97f7
Rename ReaderDiscoverViewController
kean Oct 17, 2024
751b7fc
Move ReaderDiscoverHeaderView
kean Oct 17, 2024
5bebf9a
Fix separator insets in ReaderPostCell
kean Oct 17, 2024
53b5a02
Extract ReaderStreamTitleView
kean Oct 17, 2024
ab39fed
Implement selection in ReaderDiscoverHeaderView
kean Oct 17, 2024
90a6003
Extract ReaderDiscoverStreamViewController to allow switching between…
kean Oct 17, 2024
85a74ab
Add initial implementation of Latest Posts
kean Oct 17, 2024
d544057
Fix how ghost cells are shown
kean Oct 17, 2024
c8871c3
Add support for tags
kean Oct 17, 2024
f8f4ed3
Rename tags to channels
kean Oct 17, 2024
14e7dfa
Integarte WPKit changes with code that loads avatars
kean Oct 18, 2024
1c30416
Add first posts
kean Oct 18, 2024
1aee40d
Add daily prompts
kean Oct 18, 2024
f8954df
Remove batchDelete
kean Oct 18, 2024
ca2381f
Add a constant for dailyprompt
kean Oct 18, 2024
8cec93b
Add ManagedObjectsObserver
kean Oct 18, 2024
19730a6
Observe tags
kean Oct 18, 2024
8259a86
Rename container to target
kean Oct 18, 2024
391871b
Add some docs
kean Oct 21, 2024
8a75e49
Add a link to edit your interests to the Reader
kean Oct 21, 2024
a57fdb1
Add runtime check for pinEdges
kean Oct 24, 2024
93d9664
Add .readerDiscoverChannelSelected
kean Oct 24, 2024
0e9f4c3
Add readerDiscoverEditInterestsTapped
kean Oct 24, 2024
32f2da8
Update tests
kean Oct 29, 2024
24d5826
Update unit tests
kean Oct 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Modules/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ let package = Package(
.package(url: "https://github.com/wordpress-mobile/MediaEditor-iOS", branch: "task/spm-support"),
.package(url: "https://github.com/wordpress-mobile/NSObject-SafeExpectations", from: "0.0.6"),
.package(url: "https://github.com/wordpress-mobile/NSURL-IDN", branch: "trunk"),
.package(url: "https://github.com/wordpress-mobile/WordPressKit-iOS", branch: "wpios-edition"),
.package(url: "https://github.com/wordpress-mobile/WordPressKit-iOS", branch: "task/reader-discover"),
.package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"),
// We can't use wordpress-rs branches nor commits here. Only tags work.
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-swift-20240813"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Foundation
import CoreData
import Combine

public final class ManagedObjectsObserver<T: NSManagedObject>: NSObject, NSFetchedResultsControllerDelegate {
@Published private(set) public var objects: [T] = []

private let controller: NSFetchedResultsController<T>

public convenience init(
predicate: NSPredicate,
sortDescriptors: [SortDescriptor<T>],
context: NSManagedObjectContext
) {
let request = NSFetchRequest<T>(entityName: T.entity().name ?? "")
request.predicate = predicate
request.sortDescriptors = sortDescriptors.map(NSSortDescriptor.init)
self.init(request: request, context: context)
}

public init(
request: NSFetchRequest<T>,
context: NSManagedObjectContext,
cacheName: String? = nil
) {
self.controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: cacheName)
super.init()

try? controller.performFetch()
objects = controller.fetchedObjects ?? []

controller.delegate = self
}

public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
objects = self.controller.fetchedObjects ?? []
}
}
14 changes: 14 additions & 0 deletions Modules/Sources/WordPressUI/Extensions/UITextView+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import UIKit

extension UITextView {
/// Creates a text view that behaves like a non-editable multiline label
/// but supports interaction and other text view features.
public static func makeLabel() -> UITextView {
let textView = UITextView()
textView.isScrollEnabled = false
textView.isEditable = false
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
return textView
}
}
116 changes: 116 additions & 0 deletions Modules/Sources/WordPressUI/Extensions/UIView+AutoLayout.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import UIKit
import SwiftUI

extension UIView {
/// Pins edges of the view to the edges of the given target view or layout
/// guide. By default, pins to the superview.
///
/// The view also gets enabled for Auto Layout by setting
/// `translatesAutoresizingMaskIntoConstraints` to `false`.
///
/// Example uage:
///
/// ```swift
/// subview.pinEdges() // to superview
/// subview.pinEdges(to: superview.safeAreaLayoutGuide)
/// ```
@discardableResult
public func pinEdges(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: update the signature to be pin(edges: .all, to: view, ...) instead of pinEdges(.all, to: view, ...)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't be clear what it does with a default argument view.pin(to: view). It has to have the default argument because you'll use .all 99% of the time.

_ edges: Edge.Set = .all,
to target: AutoLayoutItem? = nil,
insets: UIEdgeInsets = .zero,
relation: AutoLayoutPinEdgesRelation = .equal,
priority: UILayoutPriority? = nil
) -> [NSLayoutConstraint] {
guard let target = target ?? superview else {
assertionFailure("view has to be installed in the view hierarchy")
return []
}
translatesAutoresizingMaskIntoConstraints = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an assertion (self.isDescendant(of: container)) to make sure self is a subview of container, because this line may not be appropriate, depending on the relationship between self and container.

// OK to set `subview.translatesAutoresizingMaskIntoConstraints = false`
// because subview's frame is set by auto-layout.
subview.pinEdges(to: superview)

// Not okay to set `superview.translatesAutoresizingMaskIntoConstraints = false`
// because we don't know if superview's frame is set by auto-layout or not.
superview.pinEdges(to: subview)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sure self is a subview of container

It's not a strict requirement. You can pin a view to a layout guide or to a sibling view. It should be pretty clear from the method's name what it does. SnapKit, PureLayout, and a couple of other frameworks all pretty much work the same way, so it shouldn't surprise that that's how it works.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is no hierarchical requirement in these two views, we should remove this line, as I mentioned in the example code above:

// Not okay to set `superview.translatesAutoresizingMaskIntoConstraints = false`
// because we don't know if superview's frame is set by auto-layout or not.
superview.pinEdges(to: subview)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC UIViewController.view is not framed using auto layout. Maybe you can reproduce the issue I mentioned by using pinEdges(to: subview) on a UIViewController.view.

Copy link
Contributor Author

@kean kean Oct 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, you shouldn't pin the edges of the superview to its child, and I added a bit of docs explaining it. It even sounds kind of backwards, doesn't it?

The default signature view.pinEdges() enforces the idea that you are pinning it to a superview because that's the only reasonable option when you don't have a target view.

As a user, you want to avoid writing translatesAutoresizingMaskIntoConstraints. It's a pretty common mistake to forget it, which I've also accidentally done many times when using the existing pinSubviewToAllEdges methods. If you take pretty much every Auto Layout framework, they are designed to ensure you never have to write translatesAutoresizingMaskIntoConstraints. The simple rule is that the receiver of the pin* methods is the view you want to layout using Auto Layout → it's safe to set translatesAutoresizingMaskIntoConstraints to false.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a user, you want to avoid writing translatesAutoresizingMaskIntoConstraints. [...] If you take pretty much every Auto Layout framework, they are designed to ensure you never have to write translatesAutoresizingMaskIntoConstraints.

That's true. I get it's a bit annoying that we are forced to write this one long line for pretty much every single view in the app. However, the framework (or convenient method in this case) should only set the property when it's appropriate to do so. As I mentioned above, it's not appropriate to call translatesAutoresizingMaskIntoConstraints = true, if self is not framed using auto-layout.

Here is a diff to reproduce the issue:

diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderDiscoverViewController.swift b/WordPress/Classes/ViewRelated/Reader/ReaderDiscoverViewController.swift
index af9eda30d5..ffa871d863 100644
--- a/WordPress/Classes/ViewRelated/Reader/ReaderDiscoverViewController.swift
+++ b/WordPress/Classes/ViewRelated/Reader/ReaderDiscoverViewController.swift
@@ -101,6 +101,11 @@ class ReaderDiscoverViewController: UIViewController, ReaderDiscoverHeaderViewDe
         view.addSubview(streamVC.view)
         streamVC.view.pinEdges()
         streamVC.didMove(toParent: self)
+
+        let testView = UIView()
+        testView.backgroundColor = .red
+        view.insertSubview(testView, at: 0)
+        view.pinEdges(to: testView) // Reader goes blank.
     }
 
     /// TODO: (tech-debt) the app currently stores the responses from the `/discover`

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a diff to reproduce the issue:

I added a runtime check to catch this early:

#if DEBUG
        if let target = target as? UIView {
            assert(!target.isDescendant(of: self), "The target view can't be a descendant for the view")
        }
#endif


#if DEBUG
if let target = target as? UIView {
assert(!target.isDescendant(of: self), "The target view can't be a descendant for the view")
}
#endif

var constraints: [NSLayoutConstraint] = []

func pin(_ edge: Edge.Set, _ closure: @autoclosure () -> NSLayoutConstraint) {
guard edges.contains(edge) else { return }
constraints.append(closure())
}

switch relation {
case .equal:
pin(.top, topAnchor.constraint(equalTo: target.topAnchor, constant: insets.top))
pin(.trailing, trailingAnchor.constraint(equalTo: target.trailingAnchor, constant: -insets.right))
pin(.bottom, bottomAnchor.constraint(equalTo: target.bottomAnchor, constant: -insets.bottom))
pin(.leading, leadingAnchor.constraint(equalTo: target.leadingAnchor, constant: insets.left))
case .lessThanOrEqual:
pin(.top, topAnchor.constraint(greaterThanOrEqualTo: target.topAnchor, constant: insets.top))
pin(.trailing, trailingAnchor.constraint(lessThanOrEqualTo: target.trailingAnchor, constant: -insets.right))
pin(.bottom, bottomAnchor.constraint(lessThanOrEqualTo: target.bottomAnchor, constant: -insets.bottom))
pin(.leading, leadingAnchor.constraint(greaterThanOrEqualTo: target.leadingAnchor, constant: insets.left))
}

if let priority {
for constraint in constraints {
constraint.priority = priority
}
}

NSLayoutConstraint.activate(constraints)
return constraints
}

/// Pins the view to the center of the given container. By default,
/// pins to the superview.
@discardableResult
public func pinCenter(
to target: AutoLayoutItem? = nil,
offset: UIOffset = .zero,
priority: UILayoutPriority? = nil
) -> [NSLayoutConstraint] {
guard let target = target ?? superview else {
assertionFailure("view has to be installed in the view hierarchy")
return []
}
translatesAutoresizingMaskIntoConstraints = false

let constraints = [
centerXAnchor.constraint(equalTo: target.centerXAnchor, constant: offset.horizontal),
centerYAnchor.constraint(equalTo: target.centerYAnchor, constant: offset.vertical),
]

if let priority {
for constraint in constraints {
constraint.priority = priority
}
}

NSLayoutConstraint.activate(constraints)
return constraints
}
}

public protocol AutoLayoutItem {
var leadingAnchor: NSLayoutXAxisAnchor { get }
var trailingAnchor: NSLayoutXAxisAnchor { get }
var leftAnchor: NSLayoutXAxisAnchor { get }
var rightAnchor: NSLayoutXAxisAnchor { get }
var topAnchor: NSLayoutYAxisAnchor { get }
var bottomAnchor: NSLayoutYAxisAnchor { get }
var widthAnchor: NSLayoutDimension { get }
var heightAnchor: NSLayoutDimension { get }
var centerXAnchor: NSLayoutXAxisAnchor { get }
var centerYAnchor: NSLayoutYAxisAnchor { get }
}

public enum AutoLayoutPinEdgesRelation {
case equal
case lessThanOrEqual
}

extension UIView: AutoLayoutItem {}
extension UILayoutGuide: AutoLayoutItem {}
13 changes: 2 additions & 11 deletions Modules/Sources/WordPressUI/Extensions/UIView+Helpers.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Foundation
import UIKit

// MARK: - UIView Helpers
//
// MARK: - UIView (Soft-Deprecated)

extension UIView {

@objc public func pinSubviewAtCenter(_ subview: UIView) {
Expand Down Expand Up @@ -89,13 +89,4 @@ extension UIView {
@objc public func userInterfaceLayoutDirection() -> UIUserInterfaceLayoutDirection {
return UIView.userInterfaceLayoutDirection(for: semanticContentAttribute)
}

public func changeLayoutMargins(top: CGFloat? = nil, left: CGFloat? = nil, bottom: CGFloat? = nil, right: CGFloat? = nil) {
let top = top ?? layoutMargins.top
let left = left ?? layoutMargins.left
let bottom = bottom ?? layoutMargins.bottom
let right = right ?? layoutMargins.right

layoutMargins = UIEdgeInsets(top: top, left: left, bottom: bottom, right: right)
}
}

This file was deleted.

4 changes: 2 additions & 2 deletions WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions WordPress/Classes/Extensions/Font/UIFont+Weight.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ extension UIFont {
return UIFont(descriptor: descriptor, size: 0)
}

private func withWeight(_ weight: UIFont.Weight) -> UIFont {
func withWeight(_ weight: UIFont.Weight) -> UIFont {
let descriptor = fontDescriptor.addingAttributes([.traits: [UIFontDescriptor.TraitKey.weight: weight]])

return UIFont(descriptor: descriptor, size: 0)
}
}
4 changes: 4 additions & 0 deletions WordPress/Classes/Models/ReaderTagTopic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,7 @@ import Foundation
showInMenu = (following || isRecommended)
}
}

extension ReaderTagTopic {
static let dailyPromptTag = "dailyprompt"
}
58 changes: 28 additions & 30 deletions WordPress/Classes/Services/ReaderCardService.swift
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
import Foundation
import WordPressKit

protocol ReaderCardServiceRemote {

func fetchStreamCards(for topics: [String],
func fetchStreamCards(stream: ReaderStream,
for topics: [String],
page: String?,
sortingOption: ReaderSortingOption,
refreshCount: Int?,
count: Int?,
success: @escaping ([RemoteReaderCard], String?) -> Void,
failure: @escaping (Error) -> Void)

func fetchCards(for topics: [String],
page: String?,
sortingOption: ReaderSortingOption,
refreshCount: Int?,
success: @escaping ([RemoteReaderCard], String?) -> Void,
failure: @escaping (Error) -> Void)

}

extension ReaderPostServiceRemote: ReaderCardServiceRemote { }

class ReaderCardService {
private let stream: ReaderStream
private let sorting: ReaderSortingOption
private let service: ReaderCardServiceRemote

private let coreDataStack: CoreDataStack
Expand All @@ -35,10 +31,14 @@ class ReaderCardService {
/// Used only internally to order the cards
private var pageNumber = 1

init(service: ReaderCardServiceRemote = ReaderPostServiceRemote.withDefaultApi(),
init(stream: ReaderStream = .discover,
sorting: ReaderSortingOption = .noSorting,
service: ReaderCardServiceRemote = ReaderPostServiceRemote.withDefaultApi(),
coreDataStack: CoreDataStack = ContextManager.shared,
followedInterestsService: ReaderFollowedInterestsService? = nil,
siteInfoService: ReaderSiteInfoService? = nil) {
self.stream = stream
self.sorting = sorting
self.service = service
self.coreDataStack = coreDataStack
self.followedInterestsService = followedInterestsService ?? ReaderTopicService(coreDataStack: coreDataStack)
Expand Down Expand Up @@ -72,7 +72,7 @@ class ReaderCardService {
self.coreDataStack.performAndSave({ context in
if isFirstPage {
self.pageNumber = 1
self.removeAllCards(in: context)
ReaderCardService.removeAllCards(in: context)
} else {
self.pageNumber += 1
}
Expand Down Expand Up @@ -114,33 +114,31 @@ class ReaderCardService {
failure(error)
}

if RemoteFeatureFlag.readerDiscoverEndpoint.enabled() {
self.service.fetchStreamCards(for: slugs,
page: self.pageHandle(isFirstPage: isFirstPage),
sortingOption: .noSorting,
refreshCount: refreshCount,
count: nil,
success: success,
failure: failure)
} else {
self.service.fetchCards(for: slugs,
page: self.pageHandle(isFirstPage: isFirstPage),
sortingOption: .noSorting,
refreshCount: refreshCount,
success: success,
failure: failure)
}
self.service.fetchStreamCards(
stream: self.stream,
for: slugs,
page: self.pageHandle(isFirstPage: isFirstPage),
sortingOption: self.sorting,
refreshCount: refreshCount,
count: nil,
success: success,
failure: failure
)
}
}

/// Remove all cards and saves the context
func clean() {
coreDataStack.performAndSave { context in
self.removeAllCards(in: context)
ReaderCardService.removeAllCards(on: coreDataStack)
}

static func removeAllCards(on stack: CoreDataStack = ContextManager.shared) {
stack.performAndSave { context in
removeAllCards(in: context)
}
}

private func removeAllCards(in context: NSManagedObjectContext) {
private static func removeAllCards(in context: NSManagedObjectContext) {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: ReaderCard.classNameWithoutNamespaces())
fetchRequest.returnsObjectsAsFaults = false

Expand Down
Loading