Last active
April 8, 2023 06:23
-
-
Save yosshi4486/852376ccb0fc934b5f3288ff5d7f3799 to your computer and use it in GitHub Desktop.
Swift implementation of a `BackForwardList` which can manage back and forward.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// BackForwardList.swift | |
// | |
// Created by yosshi4486 on 2023/04/02. | |
// | |
import Foundation | |
/// 進む・戻る管理の可能なリスト | |
/// | |
/// WKBackForwardListにインスパイアされて実装 | |
/// https://developer.apple.com/documentation/webkit/wkbackforwardlist | |
struct BackForwardList<Item>: Collection where Item: Hashable { | |
/// 内部ストレージ | |
private var internalStorage: Array<Item> = .init() | |
/// 続いて同じアイテムを保持することを許容するかどうかのBool値. | |
/// | |
/// この値が`false`のとき、`add(_:)`で`currentItem`と同じハッシュ値の要素を渡すと、その要素は追加されない | |
var allowsConsecutiveItems: Bool = true | |
/// `currentItem`のリスト内でのindex. アイテムが存在しない場合は-1を返し、存在する場合は0以上で1づつ増加して返す | |
private(set) var currentItemIndex: Int = -1 | |
/// リスト内の`currentItemIndex`が指す要素. データが空の場合`nil`を返す | |
var currentItem: Item? { | |
guard !internalStorage.isEmpty else { | |
return nil | |
} | |
return internalStorage[currentItemIndex] | |
} | |
/// 現在のアイテムより1つ前にあるアイテム | |
var backItem: Item? { backItems.first } | |
/// 現在のアイテムより1つ次にあるアイテム | |
var forwardItem: Item? { forwardItems.first } | |
/// 現在のアイテムより前にあるアイテムの配列. 先頭ほど`currentItem`に近く、末尾ほど`currenItem`から遠いアイテムを指す | |
/// | |
/// このリスト内のアイテムの`currentItemIndex`より前の要素を返す. データが空、`canBack`が`false`の場合は空配列を返す | |
var backItems: [Item] { | |
guard !internalStorage.isEmpty, canBack else { | |
return [] | |
} | |
return Array(internalStorage[startIndex..<currentItemIndex]).reversed() | |
} | |
/// 現在のアイテムより後ろにあるアイテムの配列. 先頭ほど`currentItem`に近く、末尾ほど`currenItem`から遠いアイテムを指す | |
/// | |
/// このリスト内のアイテムの`currentItemIndex`より後の要素を返す. データが空、`canForward`が`false`の場合は空配列を返す. | |
var forwardItems: [Item] { | |
guard !internalStorage.isEmpty, canForward else { | |
return [] | |
} | |
return Array(internalStorage[(currentItemIndex + 1)..<endIndex]) | |
} | |
var startIndex: Int { | |
return internalStorage.startIndex | |
} | |
var endIndex: Int { | |
return internalStorage.endIndex | |
} | |
/// 履歴内の1つ前のアイテムに戻れるかどうかのBool値. 戻れれば`true`、そうでなければ`false`. | |
var canBack: Bool { | |
return (currentItemIndex - 1) >= startIndex | |
} | |
/// 履歴内の1つ次のアイテムに進めるかどうかのBool値. 進めればば`true`、そうでなければ`false`. | |
var canForward: Bool { | |
return (currentItemIndex + 1) < endIndex | |
} | |
/// 空の`BackForwardList`を初期化する | |
init() { | |
self.internalStorage = [] | |
} | |
/// 与えられた`items`でリストを初期化する | |
/// | |
/// - Parameter items: BackForwardListのHashableな要素. | |
init(items: [Item]) { | |
self.internalStorage = items | |
self.currentItemIndex = items.endIndex - 1 | |
} | |
subscript(position: Int) -> Item { | |
return internalStorage[position] | |
} | |
func index(after i: Int) -> Int { | |
return internalStorage.index(after: i) | |
} | |
/// 指定した`item`をリストに追加する. `allowsConsecutiveItems`が`true`の場合、`currentItem`と同じアイテムを渡してもリストに追加されるが、そうでない場合は無視され追加されない。 | |
/// | |
/// - Parameter item: 新規追加するアイテム. | |
mutating func add(_ item: Item) { | |
if allowsConsecutiveItems == false && item == currentItem { | |
return | |
} | |
var arraySlice = internalStorage.prefix(through: currentItemIndex) | |
arraySlice.append(item) | |
internalStorage = Array(arraySlice) | |
currentItemIndex = internalStorage.endIndex - 1 | |
} | |
/// `canBack`を満たす場合のみ、リストの現在指定位置を1つ戻す. | |
mutating func back() { | |
guard canBack else { return } | |
currentItemIndex -= 1 | |
} | |
/// `canForward`を満たす場合のみ、リストの現在指定位置を1つ進める. | |
mutating func forward() { | |
guard canForward else { return } | |
currentItemIndex += 1 | |
} | |
/// 与えられた`item`を`currentItem`に設定する. | |
/// | |
/// このリスト内の要素を`item`に渡した場合、`currentItem`がその`item`を指すようになり、`true`を返す。 | |
/// このリスト内に存在しない要素を`item`に渡した場合、`false`を返す. | |
/// | |
/// - Parameter item: `currentItem`に設定したいアイテム. | |
@discardableResult mutating func makeCurrentItem(_ item: Item) -> Bool { | |
if let newCurrentItemIndex = internalStorage.firstIndex(of: item) { | |
currentItemIndex = newCurrentItemIndex | |
return true | |
} else { | |
return false | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// BackForwardListTests.swift | |
// | |
// Created by yosshi4486 on 2023/04/02. | |
// | |
import XCTest | |
@testable import {ProjectName} | |
final class BackForwardListTests: XCTestCase { | |
override func setUpWithError() throws { | |
// Put setup code here. This method is called before the invocation of each test method in the class. | |
} | |
override func tearDownWithError() throws { | |
// Put teardown code here. This method is called after the invocation of each test method in the class. | |
} | |
func testInitItems() throws { | |
let backForwardList = BackForwardList<String>(items: ["Item A", "Item B", "Item C"]) | |
XCTAssertEqual(backForwardList.currentItemIndex, 2) | |
XCTAssertEqual(backForwardList.currentItem, "Item C") | |
XCTAssertTrue(backForwardList.canBack) | |
XCTAssertFalse(backForwardList.canForward) | |
XCTAssertEqual(backForwardList.backItem, "Item B") | |
XCTAssertEqual(backForwardList.backItems, ["Item B", "Item A"]) | |
XCTAssertNil(backForwardList.forwardItem) | |
XCTAssertEqual(backForwardList.forwardItems, []) | |
XCTAssertEqual(backForwardList[0], "Item A") | |
XCTAssertEqual(backForwardList[1], "Item B") | |
XCTAssertEqual(backForwardList[2], "Item C") | |
} | |
func testAddItem() throws { | |
var backForwardList = BackForwardList<String>() | |
XCTAssertEqual(backForwardList.currentItemIndex, -1) | |
XCTAssertNil(backForwardList.currentItem) | |
XCTAssertFalse(backForwardList.canBack) | |
XCTAssertFalse(backForwardList.canForward) | |
XCTAssertNil(backForwardList.backItem) | |
XCTAssertEqual(backForwardList.backItems, []) | |
XCTAssertNil(backForwardList.forwardItem) | |
XCTAssertEqual(backForwardList.forwardItems, []) | |
backForwardList.add("Item A") | |
XCTAssertEqual(backForwardList.currentItemIndex, 0) | |
XCTAssertEqual(backForwardList.currentItem, "Item A") | |
XCTAssertFalse(backForwardList.canBack) | |
XCTAssertFalse(backForwardList.canForward) | |
XCTAssertNil(backForwardList.backItem) | |
XCTAssertEqual(backForwardList.backItems, []) | |
XCTAssertNil(backForwardList.forwardItem) | |
XCTAssertEqual(backForwardList.forwardItems, []) | |
backForwardList.add("Item B") | |
XCTAssertEqual(backForwardList.currentItemIndex, 1) | |
XCTAssertEqual(backForwardList.currentItem, "Item B") | |
XCTAssertTrue(backForwardList.canBack) | |
XCTAssertFalse(backForwardList.canForward) | |
XCTAssertEqual(backForwardList.backItem, "Item A") | |
XCTAssertEqual(backForwardList.backItems, ["Item A"]) | |
XCTAssertNil(backForwardList.forwardItem) | |
XCTAssertEqual(backForwardList.forwardItems, []) | |
backForwardList.add("Item C") | |
XCTAssertEqual(backForwardList.currentItemIndex, 2) | |
XCTAssertEqual(backForwardList.currentItem, "Item C") | |
XCTAssertTrue(backForwardList.canBack) | |
XCTAssertFalse(backForwardList.canForward) | |
XCTAssertEqual(backForwardList.backItem, "Item B") | |
XCTAssertEqual(backForwardList.backItems, ["Item B", "Item A"]) | |
XCTAssertNil(backForwardList.forwardItem) | |
XCTAssertEqual(backForwardList.forwardItems, []) | |
} | |
func testAddItemWhenAllowConsecutiveItemsTrue() { | |
var backForwardList = BackForwardList<String>(items: ["Item A"]) | |
backForwardList.allowsConsecutiveItems = true | |
backForwardList.add("Item A") | |
XCTAssertEqual(backForwardList.currentItemIndex, 1) | |
XCTAssertEqual(backForwardList.currentItem, "Item A") | |
XCTAssertTrue(backForwardList.canBack) | |
XCTAssertFalse(backForwardList.canForward) | |
XCTAssertEqual(backForwardList.backItem, "Item A") | |
XCTAssertEqual(backForwardList.backItems, ["Item A"]) | |
XCTAssertNil(backForwardList.forwardItem) | |
XCTAssertEqual(backForwardList.forwardItems, []) | |
XCTAssertEqual(backForwardList.count, 2) | |
XCTAssertEqual(backForwardList[0], "Item A") | |
XCTAssertEqual(backForwardList[1], "Item A") | |
} | |
func testAddItemWhenAllowConsecutiveItemsFalse() { | |
var backForwardList = BackForwardList<String>(items: ["Item A"]) | |
backForwardList.allowsConsecutiveItems = false | |
backForwardList.add("Item A") | |
XCTAssertEqual(backForwardList.currentItemIndex, 0) | |
XCTAssertEqual(backForwardList.currentItem, "Item A") | |
XCTAssertFalse(backForwardList.canBack) | |
XCTAssertFalse(backForwardList.canForward) | |
XCTAssertNil(backForwardList.backItem) | |
XCTAssertEqual(backForwardList.backItems, []) | |
XCTAssertNil(backForwardList.forwardItem) | |
XCTAssertEqual(backForwardList.forwardItems, []) | |
XCTAssertEqual(backForwardList.count, 1) | |
XCTAssertEqual(backForwardList[0], "Item A") | |
} | |
/// - Precondition: `testInitItems`と`testBack`はパスしていることとする. | |
func testAddItemWhenCurrentItemIsHalfPosition() { | |
var backForwardList = BackForwardList<String>(items: ["Item A", "Item B", "Item C"]) | |
backForwardList.back() | |
backForwardList.back() | |
backForwardList.add("Item D") | |
XCTAssertEqual(backForwardList.currentItemIndex, 1) | |
XCTAssertEqual(backForwardList.currentItem, "Item D") | |
XCTAssertTrue(backForwardList.canBack) | |
XCTAssertFalse(backForwardList.canForward) | |
XCTAssertEqual(backForwardList.backItem, "Item A") | |
XCTAssertEqual(backForwardList.backItems, ["Item A"]) | |
XCTAssertNil(backForwardList.forwardItem) | |
XCTAssertEqual(backForwardList.forwardItems, []) | |
} | |
// curretItemIndexはprivate(set)で動かすにはback()とforward()を使うしかないのでまとめたテストになってしまう。 | |
/// - Precondition: `testInitItems`はパスしていることとする. | |
func testBackAndForward() { | |
var backForwardList = BackForwardList<String>(items: ["Item A", "Item B", "Item C"]) | |
backForwardList.back() | |
XCTAssertEqual(backForwardList.currentItemIndex, 1) | |
XCTAssertEqual(backForwardList.currentItem, "Item B") | |
XCTAssertTrue(backForwardList.canBack) | |
XCTAssertTrue(backForwardList.canForward) | |
XCTAssertEqual(backForwardList.backItem, "Item A") | |
XCTAssertEqual(backForwardList.backItems, ["Item A"]) | |
XCTAssertEqual(backForwardList.forwardItem, "Item C") | |
XCTAssertEqual(backForwardList.forwardItems, ["Item C"]) | |
backForwardList.back() | |
XCTAssertEqual(backForwardList.currentItemIndex, 0) | |
XCTAssertEqual(backForwardList.currentItem, "Item A") | |
XCTAssertFalse(backForwardList.canBack) | |
XCTAssertTrue(backForwardList.canForward) | |
XCTAssertNil(backForwardList.backItem) | |
XCTAssertEqual(backForwardList.backItems, []) | |
XCTAssertEqual(backForwardList.forwardItem, "Item B") | |
XCTAssertEqual(backForwardList.forwardItems, ["Item B", "Item C"]) | |
// canBackがfalseの場合の実行はどうか。 | |
backForwardList.back() | |
XCTAssertEqual(backForwardList.currentItemIndex, 0) | |
XCTAssertEqual(backForwardList.currentItem, "Item A") | |
XCTAssertFalse(backForwardList.canBack) | |
XCTAssertTrue(backForwardList.canForward) | |
XCTAssertNil(backForwardList.backItem) | |
XCTAssertEqual(backForwardList.backItems, []) | |
XCTAssertEqual(backForwardList.forwardItem, "Item B") | |
XCTAssertEqual(backForwardList.forwardItems, ["Item B", "Item C"]) | |
backForwardList.forward() | |
XCTAssertEqual(backForwardList.currentItemIndex, 1) | |
XCTAssertEqual(backForwardList.currentItem, "Item B") | |
XCTAssertTrue(backForwardList.canBack) | |
XCTAssertTrue(backForwardList.canForward) | |
XCTAssertEqual(backForwardList.backItem, "Item A") | |
XCTAssertEqual(backForwardList.backItems, ["Item A"]) | |
XCTAssertEqual(backForwardList.forwardItem, "Item C") | |
XCTAssertEqual(backForwardList.forwardItems, ["Item C"]) | |
backForwardList.forward() | |
XCTAssertEqual(backForwardList.currentItemIndex, 2) | |
XCTAssertEqual(backForwardList.currentItem, "Item C") | |
XCTAssertTrue(backForwardList.canBack) | |
XCTAssertFalse(backForwardList.canForward) | |
XCTAssertEqual(backForwardList.backItem, "Item B") | |
XCTAssertEqual(backForwardList.backItems, ["Item B", "Item A"]) | |
XCTAssertNil(backForwardList.forwardItem) | |
XCTAssertEqual(backForwardList.forwardItems, []) | |
// canForwardがfalseの場合の実行はどうか。 | |
backForwardList.forward() | |
XCTAssertEqual(backForwardList.currentItemIndex, 2) | |
XCTAssertEqual(backForwardList.currentItem, "Item C") | |
XCTAssertTrue(backForwardList.canBack) | |
XCTAssertFalse(backForwardList.canForward) | |
XCTAssertEqual(backForwardList.backItem, "Item B") | |
XCTAssertEqual(backForwardList.backItems, ["Item B", "Item A"]) | |
XCTAssertNil(backForwardList.forwardItem) | |
XCTAssertEqual(backForwardList.forwardItems, []) | |
} | |
func testMakeCurrentItem() { | |
var backForwardList = BackForwardList<String>(items: ["Item A", "Item B", "Item C"]) | |
let resultA = backForwardList.makeCurrentItem("Item A") | |
XCTAssertTrue(resultA) | |
XCTAssertEqual(backForwardList.currentItemIndex, 0) | |
XCTAssertEqual(backForwardList.currentItem, "Item A") | |
XCTAssertFalse(backForwardList.canBack) | |
XCTAssertTrue(backForwardList.canForward) | |
XCTAssertNil(backForwardList.backItem) | |
XCTAssertEqual(backForwardList.backItems, []) | |
XCTAssertEqual(backForwardList.forwardItem, "Item B") | |
XCTAssertEqual(backForwardList.forwardItems, ["Item B", "Item C"]) | |
let resultC = backForwardList.makeCurrentItem("Item C") | |
XCTAssertTrue(resultC) | |
XCTAssertEqual(backForwardList.currentItemIndex, 2) | |
XCTAssertEqual(backForwardList.currentItem, "Item C") | |
XCTAssertTrue(backForwardList.canBack) | |
XCTAssertFalse(backForwardList.canForward) | |
XCTAssertEqual(backForwardList.backItem, "Item B") | |
XCTAssertEqual(backForwardList.backItems, ["Item B", "Item A"]) | |
XCTAssertNil(backForwardList.forwardItem) | |
XCTAssertEqual(backForwardList.forwardItems, []) | |
let resultB = backForwardList.makeCurrentItem("Item B") | |
XCTAssertTrue(resultB) | |
XCTAssertEqual(backForwardList.currentItemIndex, 1) | |
XCTAssertEqual(backForwardList.currentItem, "Item B") | |
XCTAssertTrue(backForwardList.canBack) | |
XCTAssertTrue(backForwardList.canForward) | |
XCTAssertEqual(backForwardList.backItem, "Item A") | |
XCTAssertEqual(backForwardList.backItems, ["Item A"]) | |
XCTAssertEqual(backForwardList.forwardItem, "Item C") | |
XCTAssertEqual(backForwardList.forwardItems, ["Item C"]) | |
let resultUnknown = backForwardList.makeCurrentItem("Item X") | |
XCTAssertFalse(resultUnknown) | |
XCTAssertEqual(backForwardList.currentItemIndex, 1) | |
XCTAssertEqual(backForwardList.currentItem, "Item B") | |
XCTAssertTrue(backForwardList.canBack) | |
XCTAssertTrue(backForwardList.canForward) | |
XCTAssertEqual(backForwardList.backItem, "Item A") | |
XCTAssertEqual(backForwardList.backItems, ["Item A"]) | |
XCTAssertEqual(backForwardList.forwardItem, "Item C") | |
XCTAssertEqual(backForwardList.forwardItems, ["Item C"]) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment