Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions WHATSNEW.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Version 2022.3
* The Recently Played section now scrolls horizontally and supports up to 100 entries.
* Updated the expanding/collapsing UI so that it can be expanded/collapsed using a +/- button on the right side, instead of a chevron in the header. You can still tap the header to expand/collapse as well.
* Added prefetching support: images are now cached using prefetching for improved performance.
* Changed font of header to be Bold instead of Heavy

# Version 2022.2
* Updated to [MAME 244](https://www.mamedev.org/releases/whatsnew_0244.txt).
* hi-res font for `MAME` Config Menu.
Expand Down
26 changes: 26 additions & 0 deletions xcode/MAME4iOS/ChooseGameController+Prefetching.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// ChooseGameController+Prefetching.swift
// MAME4iOS
//
// Created by Yoshi Sugawara on 5/31/22.
// Copyright © 2022 MAME4iOS Team. All rights reserved.
//

extension ChooseGameController: UICollectionViewDataSourcePrefetching {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am worried that iOS is gonna go crazy pre-loading and we will fire off a lot of net requests, we can't cancel

public func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
print("ChooseGameController: Start prefetch of images for indexPaths: \(indexPaths)")
for indexPath in indexPaths {
guard let game = getGameInfo(indexPath),
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you can just call the gameLoadImage (same thing when setting up cell) and ignore the result

let imageUrl = game.gameImageURLs.first else {
continue
}
guard ImageCache.sharedInstance().getImage(imageUrl) == nil else {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should add a NS_SWIFT_NAME so it is just ImageCache.shared

print("image already in cache, not fetching...")
continue
}
ImageCache.sharedInstance().getImage(imageUrl, localURL: game.gameLocalImageURL) { _ in
print("Prefetch: fetched image and put in cache")
}
}
}
}
1 change: 1 addition & 0 deletions xcode/MAME4iOS/ChooseGameController.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN

+(NSAttributedString*)getGameText:(GameInfo*)game;
+(UIImage*)getGameIcon:(GameInfo*)game;
-( GameInfo* _Nullable)getGameInfo:(NSIndexPath*)indexPath;

@end

Expand Down
192 changes: 142 additions & 50 deletions xcode/MAME4iOS/ChooseGameController.m

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions xcode/MAME4iOS/Collection+Safe.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// Collection+Safe.swift
// MAME4iOS
//
// Created by Yoshi Sugawara on 5/31/22.
// Copyright © 2022 MAME4iOS Team. All rights reserved.
//

import Foundation

extension Collection {
/// Returns the element at the specified index if it is within bounds, otherwise nil.
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
36 changes: 35 additions & 1 deletion xcode/MAME4iOS/GameInfoCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class GameInfoCell : UICollectionViewCell {
override func layoutSubviews() {
super.layoutSubviews()

var rect = bounds
var rect = bounds.inset(by: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 32))
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't like the hard coded constants of 30 and 28 should be based on the height of the cell, or line height of the font

ALao what happens on tvOS, do we still have these buttons?


if let size = image.image?.size, size != .zero {
// use imageAspect unless it is zero
Expand Down Expand Up @@ -243,11 +243,45 @@ class GameInfoCell : UICollectionViewCell {
// MARK: GameInfoHeader - same as GameInfoCell

class GameInfoHeader : GameInfoCell {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I was expecting this header cell to get used like this!

let expandCollapseButton: UIButton = {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
button.contentMode = .scaleAspectFill
button.contentHorizontalAlignment = .fill
button.contentVerticalAlignment = .fill
button.setImage(UIImage(systemName: "minus.square"), for: .normal)
button.setImage(UIImage(systemName: "plus.square"), for: .selected)
button.heightAnchor.constraint(equalToConstant: 28).isActive = true
button.widthAnchor.constraint(equalTo: button.heightAnchor).isActive = true
return button
}()

#if os(tvOS)
override var canBecomeFocused: Bool {
return true
}
#endif

var didToggleClosure: (() -> Void)?

override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(expandCollapseButton)
NSLayoutConstraint.activate([
expandCollapseButton.trailingAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.trailingAnchor, constant: -8),
expandCollapseButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
])
expandCollapseButton.addTarget(self, action: #selector(expandCollapseButtonPressed(_:)), for: .touchUpInside)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

@objc func expandCollapseButtonPressed(_ sender: UIButton) {
sender.isSelected.toggle()
didToggleClosure?()
}
}

// MARK: GameInfoCellLayout - a subclass that will invalidate the layout (for real) on a size change
Expand Down
1 change: 1 addition & 0 deletions xcode/MAME4iOS/MAME4iOS-Bridging-Header.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#import "InfoDatabase.h"
#import "ChooseGameController.h"
#import "EmulatorController.h"
#import "ImageCache.h"

// Globals.h stuff needed from Swift
NS_ASSUME_NONNULL_BEGIN
Expand Down
4 changes: 2 additions & 2 deletions xcode/MAME4iOS/MAME4iOS.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@

ORG_IDENTIFIER = com.example // CHANGE this to your Organization Identifier.
DEVELOPMENT_TEAM = ABC8675309 // CHANGE this to your Team ID. (or select in Xcode project editor)
CURRENT_PROJECT_VERSION = 2022.2
MARKETING_VERSION = 2022.2
CURRENT_PROJECT_VERSION = 2022.3
MARKETING_VERSION = 2022.3

// 2. enable or disable entitlements
// tvOS TopShelf and iCloud import/export require special app entitlements
Expand Down
18 changes: 18 additions & 0 deletions xcode/MAME4iOS/MAME4iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
926C771D21F5034100103EDE /* TVInputOptionsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 926C771C21F5034100103EDE /* TVInputOptionsController.m */; };
928F7B2D27D5F18E00377C40 /* CommandLineArgsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 928F7B2C27D5F18E00377C40 /* CommandLineArgsHelper.swift */; };
928F7B2E27F0223100377C40 /* CommandLineArgsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 928F7B2C27D5F18E00377C40 /* CommandLineArgsHelper.swift */; };
9293C61D284681E3002729B4 /* ChooseGameController+Prefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9293C61C284681E3002729B4 /* ChooseGameController+Prefetching.swift */; };
9293C61E284681E3002729B4 /* ChooseGameController+Prefetching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9293C61C284681E3002729B4 /* ChooseGameController+Prefetching.swift */; };
9293C62028475AB4002729B4 /* Collection+Safe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9293C61F28475AB4002729B4 /* Collection+Safe.swift */; };
9293C62128475AB4002729B4 /* Collection+Safe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9293C61F28475AB4002729B4 /* Collection+Safe.swift */; };
9296524C27FC1FF30064D1F5 /* EmulatorTouchMouse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9296524B27FC1FF30064D1F5 /* EmulatorTouchMouse.swift */; };
9296524E27FC20330064D1F5 /* EmulatorController+TouchMouse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9296524D27FC20330064D1F5 /* EmulatorController+TouchMouse.swift */; };
92A5332321EB57F00089FBB9 /* Options.m in Sources */ = {isa = PBXBuildFile; fileRef = 92A5332121EB57F00089FBB9 /* Options.m */; };
Expand Down Expand Up @@ -73,6 +77,8 @@
92A533DB21EEFDE20089FBB9 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92A533DA21EEFDE20089FBB9 /* CFNetwork.framework */; };
92A533DF21EEFE520089FBB9 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92A533DE21EEFE520089FBB9 /* CFNetwork.framework */; };
92B99F0F2022706600CC44E3 /* UIView+Toast.m in Sources */ = {isa = PBXBuildFile; fileRef = 92B99F0D2022706600CC44E3 /* UIView+Toast.m */; };
92C4164D284495AC00085A4B /* RecentlyPlayedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92C4164C284495AC00085A4B /* RecentlyPlayedCell.swift */; };
92C4164E284495AC00085A4B /* RecentlyPlayedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92C4164C284495AC00085A4B /* RecentlyPlayedCell.swift */; };
92D96574215B03E100EFE3AE /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 92D96573215B03E100EFE3AE /* libc++.tbd */; };
92ECB8F721EA985000D1E3D0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 92ECB8F621EA985000D1E3D0 /* Assets.xcassets */; };
92ECB8FF21EA992100D1E3D0 /* libmame-tvos.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 92ECB8FE21EA992100D1E3D0 /* libmame-tvos.a */; };
Expand Down Expand Up @@ -299,6 +305,8 @@
926C771B21F5034100103EDE /* TVInputOptionsController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TVInputOptionsController.h; sourceTree = "<group>"; };
926C771C21F5034100103EDE /* TVInputOptionsController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TVInputOptionsController.m; sourceTree = "<group>"; };
928F7B2C27D5F18E00377C40 /* CommandLineArgsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandLineArgsHelper.swift; sourceTree = "<group>"; };
9293C61C284681E3002729B4 /* ChooseGameController+Prefetching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChooseGameController+Prefetching.swift"; sourceTree = "<group>"; };
9293C61F28475AB4002729B4 /* Collection+Safe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Safe.swift"; sourceTree = "<group>"; };
9296524B27FC1FF30064D1F5 /* EmulatorTouchMouse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmulatorTouchMouse.swift; sourceTree = "<group>"; };
9296524D27FC20330064D1F5 /* EmulatorController+TouchMouse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmulatorController+TouchMouse.swift"; sourceTree = "<group>"; };
92A5332021EB57F00089FBB9 /* Options.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Options.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -342,6 +350,7 @@
92A533E021EEFE570089FBB9 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.1.sdk/System/Library/Frameworks/MobileCoreServices.framework; sourceTree = DEVELOPER_DIR; };
92B99F0C2022706600CC44E3 /* UIView+Toast.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+Toast.h"; sourceTree = "<group>"; };
92B99F0D2022706600CC44E3 /* UIView+Toast.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+Toast.m"; sourceTree = "<group>"; };
92C4164C284495AC00085A4B /* RecentlyPlayedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyPlayedCell.swift; sourceTree = "<group>"; };
92D96573215B03E100EFE3AE /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; };
92D96575215B041300EFE3AE /* crt1.o */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.objfile"; name = crt1.o; path = usr/lib/crt1.o; sourceTree = SDKROOT; };
92D96577215B048900EFE3AE /* crt1.3.1.o */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.objfile"; name = crt1.3.1.o; path = usr/lib/crt1.3.1.o; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -689,6 +698,7 @@
EF514446282815E4001457B1 /* GameInfoController.swift */,
EF19F22E235D1C0300C8EE7F /* ChooseGameController.h */,
EF19F229235D1BDF00C8EE7F /* ChooseGameController.m */,
9293C61C284681E3002729B4 /* ChooseGameController+Prefetching.swift */,
CEE680B41635BB4900051BC2 /* EmulatorController.h */,
CEE680B51635BB4900051BC2 /* EmulatorController.m */,
EF31447B27BB1E81002C3C6A /* ControllerButtonPress.swift */,
Expand All @@ -709,6 +719,8 @@
EF286D32253CA930007DA6D3 /* CloudSync.h */,
EF286D33253CA930007DA6D3 /* CloudSync.m */,
CEBFBC0E163C5BD300A05CD0 /* resources */,
92C4164C284495AC00085A4B /* RecentlyPlayedCell.swift */,
9293C61F28475AB4002729B4 /* Collection+Safe.swift */,
);
name = RELOADED;
sourceTree = "<group>";
Expand Down Expand Up @@ -1228,11 +1240,13 @@
925ABCA91E46DCC500997182 /* OptionsController.m in Sources */,
EFE003382638ACA000E42246 /* XmlFile.m in Sources */,
EF8EAA8A244F7F5D00DA02BB /* SteamControllerExtendedGamepad.m in Sources */,
92C4164D284495AC00085A4B /* RecentlyPlayedCell.swift in Sources */,
EF8EAA8C244F7F5D00DA02BB /* SteamControllerInput.m in Sources */,
92A533C021EBDBAD0089FBB9 /* GCDWebServerURLEncodedFormRequest.m in Sources */,
92A533D821EE601D0089FBB9 /* WebServer.m in Sources */,
EFB3C6D624890F6200F44780 /* simpleCRT.metal in Sources */,
EF31447C27BB1E81002C3C6A /* ControllerButtonPress.swift in Sources */,
9293C61D284681E3002729B4 /* ChooseGameController+Prefetching.swift in Sources */,
EF474B8B2791E4CD00578663 /* EmulatorKeyboard.swift in Sources */,
92A533BA21EBDBAD0089FBB9 /* GCDWebServerDataResponse.m in Sources */,
EF24933A236264DA00DD0A6C /* ImageCache.m in Sources */,
Expand Down Expand Up @@ -1260,6 +1274,7 @@
EF7B232C23FD0469001FF51D /* (null) in Sources */,
EF8E427D249348220049C84C /* megaTron.metal in Sources */,
92A533B421EBDBAD0089FBB9 /* GCDWebServerErrorResponse.m in Sources */,
9293C62028475AB4002729B4 /* Collection+Safe.swift in Sources */,
928F7B2D27D5F18E00377C40 /* CommandLineArgsHelper.swift in Sources */,
EF23D331243CB9CF006B3EE2 /* PopupSegmentedControl.m in Sources */,
EFF9F82A2470755800DDA88C /* MetalScreenView.m in Sources */,
Expand Down Expand Up @@ -1297,18 +1312,21 @@
92A533B821EBDBAD0089FBB9 /* GCDWebServerFileResponse.m in Sources */,
92A533C121EBDBAD0089FBB9 /* GCDWebServerURLEncodedFormRequest.m in Sources */,
92ECB91921EAD57900D1E3D0 /* UIView+Toast.m in Sources */,
9293C61E284681E3002729B4 /* ChooseGameController+Prefetching.swift in Sources */,
EF19F22D235D1BDF00C8EE7F /* ChooseGameController.m in Sources */,
928F7B2E27F0223100377C40 /* CommandLineArgsHelper.swift in Sources */,
EF8EAA89244F7F5D00DA02BB /* SteamController.m in Sources */,
92A533A621EBDBAD0089FBB9 /* GCDWebServerResponse.m in Sources */,
92A533D921EE601D0089FBB9 /* WebServer.m in Sources */,
EFE0033E2638D97E00E42246 /* SoftwareList.m in Sources */,
92C4164E284495AC00085A4B /* RecentlyPlayedCell.swift in Sources */,
EFB7B21D247ACFE300AD96A4 /* MameShaders.metal in Sources */,
92A533D421EBDDF20089FBB9 /* GCDWebUploader.m in Sources */,
92ECB91221EAA1B700D1E3D0 /* EmulatorController.m in Sources */,
EF8EAA87244F7F5D00DA02BB /* SteamControllerManager.m in Sources */,
EF514448282815E4001457B1 /* GameInfoController.swift in Sources */,
EF24BAFD24C786A900D9C55A /* SkinManager.m in Sources */,
9293C62128475AB4002729B4 /* Collection+Safe.swift in Sources */,
92A533AC21EBDBAD0089FBB9 /* GCDWebServerFunctions.m in Sources */,
EF51444B282C5C68001457B1 /* GameInfo.swift in Sources */,
EF7B232D23FD0469001FF51D /* (null) in Sources */,
Expand Down
104 changes: 104 additions & 0 deletions xcode/MAME4iOS/RecentlyPlayedCell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//
// RecentlyPlayedCell.swift
// MAME4iOS
//
// Created by Yoshi Sugawara on 5/29/22.
// Copyright © 2022 MAME4iOS Team. All rights reserved.
//

import UIKit

@objcMembers class RecentlyPlayedCell: UICollectionViewCell {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I have a lot of experience with nested UICollectionView's and I am surprised how well they work, this looks good.

I like how you did not duplicate all the cell setup, and just made this a container for "the same cells" from the parent.

static let identifier = "RecentlyPlayedCell"

var items = [GameInfo]() {
didSet {
collectionView.reloadData()
}
}

let collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
return collectionView
}()

var itemSize = CGSize.zero {
didSet {
guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else {
return
}
layout.itemSize = itemSize
}
}
var setupCellClosure: ((GameInfoCell, IndexPath) -> Void)?
var selectItemClosure: ((IndexPath) -> Void)?
var contextMenuClosure: ((IndexPath) -> UIContextMenuConfiguration?)?
var heightForGameInfoClosure: ((GameInfo, UICollectionViewLayout) -> CGPoint)?

override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func setupViews() {
backgroundColor = .clear
contentView.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is This is really better than a A single line

collectionView.frame = contentView.bounds

collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
collectionView.topAnchor.constraint(equalTo: contentView.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
])
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(GameInfoCell.self, forCellWithReuseIdentifier: Self.identifier)
}
}

extension RecentlyPlayedCell: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Self.identifier, for: indexPath) as? GameInfoCell else {
return UICollectionViewCell()
}
setupCellClosure?(cell, indexPath)
return cell
}
}

extension RecentlyPlayedCell: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectItemClosure?(indexPath)
}

func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
return contextMenuClosure?(indexPath)
}
}

extension RecentlyPlayedCell: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
guard let layout = collectionViewLayout as? UICollectionViewFlowLayout,
let heightForGameInfoClosure = heightForGameInfoClosure,
let game = items[safe: indexPath.row] else {
return .zero
}
let heightInfo = heightForGameInfoClosure(game, layout)
let height = heightInfo.x + heightInfo.y
return CGSize(width: layout.itemSize.width, height: height)
}
}