Compare commits
6 Commits
10239d14c9
...
d05275020f
Author | SHA1 | Date |
---|---|---|
Shadowfacts | d05275020f | |
Shadowfacts | c420c236d9 | |
Shadowfacts | d5433e9b91 | |
Shadowfacts | cbbe9ec11f | |
Shadowfacts | 0e06d47687 | |
Shadowfacts | c907b7257a |
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -1,5 +1,23 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2022.1 (41)
|
||||||
|
Features/Improvements:
|
||||||
|
- Rewrite profile screens to use new timeline implementation
|
||||||
|
- Disable Infinite Scrolling preference (Preferences -> Digital Wellness) now applies to profiles
|
||||||
|
- Improve behavior when switching tabs on profiles
|
||||||
|
- Improve pointer interaction on timeline status cells
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
- Fix crash when loading images in certain circumstances
|
||||||
|
- Fix gallery dismissal leaving status bar hidden and breaking future gallery dismisses
|
||||||
|
- Fix timeline scroll position changing after dismissing gallery
|
||||||
|
- Fix images flickering when switching back to the Home tab
|
||||||
|
- Fix crash reporter being dismissed when sending email is cancelled
|
||||||
|
- Fix crash when long pressing Send Report button in crash reporter on iPad
|
||||||
|
- Fix Live Text controls not hiding when other gallery controls are hidden
|
||||||
|
- Fix replies appearing multiple times in Drafts list
|
||||||
|
- Fix crash when displaying blur hash images on Pleroma
|
||||||
|
|
||||||
## 2022.1 (40)
|
## 2022.1 (40)
|
||||||
Bugfixes:
|
Bugfixes:
|
||||||
- Fix selecting reblogged statuses in the timeline
|
- Fix selecting reblogged statuses in the timeline
|
||||||
|
|
|
@ -2217,7 +2217,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 40;
|
CURRENT_PROJECT_VERSION = 41;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2246,7 +2246,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Tusker/Tusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 40;
|
CURRENT_PROJECT_VERSION = 41;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = Tusker/Info.plist;
|
INFOPLIST_FILE = Tusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
|
@ -2356,7 +2356,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 40;
|
CURRENT_PROJECT_VERSION = 41;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
@ -2383,7 +2383,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
CODE_SIGN_ENTITLEMENTS = OpenInTusker/OpenInTusker.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 40;
|
CURRENT_PROJECT_VERSION = 41;
|
||||||
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
DEVELOPMENT_TEAM = V4WK9KR9U2;
|
||||||
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
INFOPLIST_FILE = OpenInTusker/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
|
|
@ -147,19 +147,20 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
private func upsert(account: Account) -> AccountMO {
|
private func upsert(account: Account, in context: NSManagedObjectContext) -> AccountMO {
|
||||||
if let accountMO = self.account(for: account.id, in: self.backgroundContext) {
|
if let accountMO = self.account(for: account.id, in: context) {
|
||||||
accountMO.updateFrom(apiAccount: account, container: self)
|
accountMO.updateFrom(apiAccount: account, container: self)
|
||||||
return accountMO
|
return accountMO
|
||||||
} else {
|
} else {
|
||||||
return AccountMO(apiAccount: account, container: self, context: self.backgroundContext)
|
return AccountMO(apiAccount: account, container: self, context: context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addOrUpdate(account: Account, completion: ((AccountMO) -> Void)? = nil) {
|
func addOrUpdate(account: Account, in context: NSManagedObjectContext? = nil, completion: ((AccountMO) -> Void)? = nil) {
|
||||||
backgroundContext.perform {
|
let context = context ?? backgroundContext
|
||||||
let accountMO = self.upsert(account: account)
|
context.perform {
|
||||||
self.save(context: self.backgroundContext)
|
let accountMO = self.upsert(account: account, in: context)
|
||||||
|
self.save(context: context)
|
||||||
completion?(accountMO)
|
completion?(accountMO)
|
||||||
self.accountSubject.send(account.id)
|
self.accountSubject.send(account.id)
|
||||||
}
|
}
|
||||||
|
@ -199,7 +200,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
|
|
||||||
func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
|
func addAll(accounts: [Account], completion: (() -> Void)? = nil) {
|
||||||
backgroundContext.perform {
|
backgroundContext.perform {
|
||||||
accounts.forEach { self.upsert(account: $0) }
|
accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) }
|
||||||
self.save(context: self.backgroundContext)
|
self.save(context: self.backgroundContext)
|
||||||
completion?()
|
completion?()
|
||||||
accounts.forEach { self.accountSubject.send($0.id) }
|
accounts.forEach { self.accountSubject.send($0.id) }
|
||||||
|
@ -213,7 +214,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
// since the status has the same account as the notification
|
// since the status has the same account as the notification
|
||||||
let accounts = notifications.filter { $0.kind != .mention }.map { $0.account }
|
let accounts = notifications.filter { $0.kind != .mention }.map { $0.account }
|
||||||
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
||||||
accounts.forEach { self.upsert(account: $0) }
|
accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) }
|
||||||
self.save(context: self.backgroundContext)
|
self.save(context: self.backgroundContext)
|
||||||
completion?()
|
completion?()
|
||||||
statuses.forEach { self.statusSubject.send($0.id) }
|
statuses.forEach { self.statusSubject.send($0.id) }
|
||||||
|
@ -227,7 +228,7 @@ class MastodonCachePersistentStore: NSPersistentContainer {
|
||||||
var updatedStatuses = [String]()
|
var updatedStatuses = [String]()
|
||||||
|
|
||||||
block(self.backgroundContext, { (accounts) in
|
block(self.backgroundContext, { (accounts) in
|
||||||
accounts.forEach { self.upsert(account: $0) }
|
accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) }
|
||||||
updatedAccounts.append(contentsOf: accounts.map { $0.id })
|
updatedAccounts.append(contentsOf: accounts.map { $0.id })
|
||||||
}, { (statuses) in
|
}, { (statuses) in
|
||||||
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<array>
|
<array>
|
||||||
|
<string>counter\.social</string>
|
||||||
<string>gab\..+</string>
|
<string>gab\..+</string>
|
||||||
</array>
|
</array>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -18,6 +18,12 @@ fileprivate let instanceCell = "instanceCell"
|
||||||
|
|
||||||
class InstanceSelectorTableViewController: UITableViewController {
|
class InstanceSelectorTableViewController: UITableViewController {
|
||||||
|
|
||||||
|
static var blocks: [NSRegularExpression] = {
|
||||||
|
guard let path = Bundle.main.path(forResource: "DomainBlocks", ofType: "plist"),
|
||||||
|
let array = NSArray(contentsOfFile: path) as? [String] else { return [] }
|
||||||
|
return array.compactMap { try? NSRegularExpression(pattern: $0, options: .caseInsensitive) }
|
||||||
|
}()
|
||||||
|
|
||||||
weak var delegate: InstanceSelectorTableViewControllerDelegate?
|
weak var delegate: InstanceSelectorTableViewControllerDelegate?
|
||||||
|
|
||||||
var dataSource: DataSource!
|
var dataSource: DataSource!
|
||||||
|
@ -100,7 +106,7 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
loadRecommendedInstances()
|
loadRecommendedInstances()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func parseURLComponents(input: String) -> URLComponents {
|
private func parseURLComponents(input: String) -> URLComponents? {
|
||||||
// we can't just use the URLComponents(string:) initializer, because when given just a domain (w/o protocol), it interprets it as the path
|
// we can't just use the URLComponents(string:) initializer, because when given just a domain (w/o protocol), it interprets it as the path
|
||||||
var input = input
|
var input = input
|
||||||
var components = URLComponents()
|
var components = URLComponents()
|
||||||
|
@ -125,13 +131,24 @@ class InstanceSelectorTableViewController: UITableViewController {
|
||||||
components.port = Int(parts.last!)
|
components.port = Int(parts.last!)
|
||||||
}
|
}
|
||||||
components.host = input
|
components.host = input
|
||||||
|
if Self.blocks.contains(where: { $0.numberOfMatches(in: input, range: NSRange(location: 0, length: input.utf16.count)) > 0 }) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return components
|
return components
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateSpecificInstance(domain: String) {
|
private func updateSpecificInstance(domain: String) {
|
||||||
activityIndicator.startAnimating()
|
activityIndicator.startAnimating()
|
||||||
|
|
||||||
let components = parseURLComponents(input: domain)
|
guard let components = parseURLComponents(input: domain) else {
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
if snapshot.indexOfSection(.selected) != nil {
|
||||||
|
snapshot.deleteSections([.selected])
|
||||||
|
dataSource.apply(snapshot)
|
||||||
|
}
|
||||||
|
activityIndicator.stopAnimating()
|
||||||
|
return
|
||||||
|
}
|
||||||
let url = components.url!
|
let url = components.url!
|
||||||
|
|
||||||
let client = Client(baseURL: url, session: .appDefault)
|
let client = Client(baseURL: url, session: .appDefault)
|
||||||
|
|
|
@ -17,12 +17,6 @@ protocol OnboardingViewControllerDelegate {
|
||||||
|
|
||||||
class OnboardingViewController: UINavigationController {
|
class OnboardingViewController: UINavigationController {
|
||||||
|
|
||||||
static var blocks: [NSRegularExpression] = {
|
|
||||||
guard let path = Bundle.main.path(forResource: "DomainBlocks", ofType: "plist"),
|
|
||||||
let array = NSArray(contentsOfFile: path) as? [String] else { return [] }
|
|
||||||
return array.compactMap { try? NSRegularExpression(pattern: $0, options: .caseInsensitive) }
|
|
||||||
}()
|
|
||||||
|
|
||||||
var onboardingDelegate: OnboardingViewControllerDelegate?
|
var onboardingDelegate: OnboardingViewControllerDelegate?
|
||||||
|
|
||||||
var instanceSelector = InstanceSelectorTableViewController()
|
var instanceSelector = InstanceSelectorTableViewController()
|
||||||
|
|
|
@ -29,9 +29,10 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
view as! UICollectionView
|
view as! UICollectionView
|
||||||
}
|
}
|
||||||
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||||
|
|
||||||
private(set) var headerCell: ProfileHeaderCollectionViewCell?
|
private(set) var headerCell: ProfileHeaderCollectionViewCell?
|
||||||
|
|
||||||
|
private var state: State = .unloaded
|
||||||
|
|
||||||
init(accountID: String?, kind: Kind, owner: ProfileViewController) {
|
init(accountID: String?, kind: Kind, owner: ProfileViewController) {
|
||||||
self.accountID = accountID
|
self.accountID = accountID
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
|
@ -41,16 +42,6 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
|
|
||||||
self.controller = TimelineLikeController(delegate: self)
|
self.controller = TimelineLikeController(delegate: self)
|
||||||
|
|
||||||
mastodonController.persistentContainer.accountSubject
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.filter { [unowned self] in $0 == self.accountID }
|
|
||||||
.sink { [unowned self] id in
|
|
||||||
var snapshot = dataSource.snapshot()
|
|
||||||
snapshot.reconfigureItems([.header(id)])
|
|
||||||
dataSource.apply(snapshot, animatingDifferences: true)
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh Profile"))
|
addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh Profile"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +88,25 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
mastodonController.persistentContainer.accountSubject
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.filter { [unowned self] in $0 == self.accountID }
|
||||||
|
.sink { [unowned self] id in
|
||||||
|
switch state {
|
||||||
|
case .unloaded:
|
||||||
|
Task {
|
||||||
|
await load()
|
||||||
|
}
|
||||||
|
case .loading:
|
||||||
|
break
|
||||||
|
case .loaded, .addedHeader:
|
||||||
|
var snapshot = dataSource.snapshot()
|
||||||
|
snapshot.reconfigureItems([.header(id)])
|
||||||
|
dataSource.apply(snapshot, animatingDifferences: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||||
|
@ -160,20 +169,26 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
}
|
}
|
||||||
|
|
||||||
private func load() async {
|
private func load() async {
|
||||||
guard accountID != nil,
|
guard isViewLoaded,
|
||||||
await controller.state == .notLoadedInitial,
|
let accountID,
|
||||||
isViewLoaded else {
|
case .unloaded = state,
|
||||||
|
mastodonController.persistentContainer.account(for: accountID) != nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state = .loading
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
snapshot.appendSections([.header, .pinned, .statuses])
|
snapshot.appendSections([.header, .pinned, .statuses])
|
||||||
snapshot.appendItems([.header(accountID)], toSection: .header)
|
snapshot.appendItems([.header(accountID)], toSection: .header)
|
||||||
await apply(snapshot, animatingDifferences: false)
|
await apply(snapshot, animatingDifferences: false)
|
||||||
print("added header item")
|
|
||||||
|
state = .addedHeader
|
||||||
|
|
||||||
await controller.loadInitial()
|
await controller.loadInitial()
|
||||||
await tryLoadPinned()
|
await tryLoadPinned()
|
||||||
|
|
||||||
|
state = .loaded
|
||||||
}
|
}
|
||||||
|
|
||||||
private func tryLoadPinned() async {
|
private func tryLoadPinned() async {
|
||||||
|
@ -223,6 +238,15 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ProfileStatusesViewController {
|
||||||
|
enum State {
|
||||||
|
case unloaded
|
||||||
|
case loading
|
||||||
|
case addedHeader
|
||||||
|
case loaded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension ProfileStatusesViewController {
|
extension ProfileStatusesViewController {
|
||||||
enum Kind {
|
enum Kind {
|
||||||
case statuses, withReplies, onlyMedia
|
case statuses, withReplies, onlyMedia
|
||||||
|
@ -436,7 +460,7 @@ extension ProfileStatusesViewController: StatusCollectionViewCellDelegate {
|
||||||
if let indexPath = collectionView.indexPath(for: cell) {
|
if let indexPath = collectionView.indexPath(for: cell) {
|
||||||
var snapshot = dataSource.snapshot()
|
var snapshot = dataSource.snapshot()
|
||||||
snapshot.reconfigureItems([dataSource.itemIdentifier(for: indexPath)!])
|
snapshot.reconfigureItems([dataSource.itemIdentifier(for: indexPath)!])
|
||||||
dataSource.apply(snapshot, animatingDifferences: false, completion: completion)
|
dataSource.apply(snapshot, animatingDifferences: animated, completion: completion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,7 @@ class ProfileViewController: UIPageViewController {
|
||||||
let req = Client.getAccount(id: accountID)
|
let req = Client.getAccount(id: accountID)
|
||||||
let (account, _) = try await mastodonController.run(req)
|
let (account, _) = try await mastodonController.run(req)
|
||||||
let mo = await withCheckedContinuation { continuation in
|
let mo = await withCheckedContinuation { continuation in
|
||||||
mastodonController.persistentContainer.addOrUpdate(account: account) { (mo) in
|
mastodonController.persistentContainer.addOrUpdate(account: account, in: mastodonController.persistentContainer.viewContext) { (mo) in
|
||||||
continuation.resume(returning: mo)
|
continuation.resume(returning: mo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -261,7 +261,7 @@ class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollecti
|
||||||
|
|
||||||
mainContainerTopToReblogLabelConstraint = mainContainer.topAnchor.constraint(equalTo: reblogLabel.bottomAnchor, constant: 4)
|
mainContainerTopToReblogLabelConstraint = mainContainer.topAnchor.constraint(equalTo: reblogLabel.bottomAnchor, constant: 4)
|
||||||
mainContainerTopToSelfConstraint = mainContainer.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8)
|
mainContainerTopToSelfConstraint = mainContainer.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8)
|
||||||
mainContainerBottomToActionsConstraint = mainContainer.bottomAnchor.constraint(equalTo: actionsContainer.topAnchor)
|
mainContainerBottomToActionsConstraint = mainContainer.bottomAnchor.constraint(equalTo: actionsContainer.topAnchor, constant: -4)
|
||||||
mainContainerBottomToSelfConstraint = mainContainer.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -6)
|
mainContainerBottomToSelfConstraint = mainContainer.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -6)
|
||||||
|
|
||||||
let metaIndicatorsBottomConstraint = metaIndicatorsView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -6)
|
let metaIndicatorsBottomConstraint = metaIndicatorsView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -6)
|
||||||
|
|
Loading…
Reference in New Issue